---
date: '2026-07-05'
description: LEB128, zigzag, VLQ, PrefixVarint, and the rest of the family
id: notes
modified: 2026-07-06 13:05:30 GMT-04:00
tags:
  - cs
title: varints
created: '2026-07-05'
published: '2026-07-05'
pageLayout: default
slug: hinterland/prep/bt/03-varint/notes
permalink: https://aarnphm.xyz/hinterland/prep/bt/03-varint/notes.md
generator:
  quartz: v4.6.0
  hostedProvider: Cloudflare
  baseUrl: aarnphm.xyz
full: https://aarnphm.xyz/llms-full.txt
---
## route

This is the most likely live prompt.

1. Read `uvarint picture`, `encode recipe`, `decode recipe`, and `error taxonomy`.
2. Solve `encode_uvarint` and `decode_uvarint`.
3. Read `zigzag`.
4. Solve `zigzag_encode`, `zigzag_decode`, `encode_svarint`, and `decode_svarint`.
5. Solve `decode_uvarint_seq`.
6. Review [[hinterland/prep/03-varint/notes.fc]].

Depth: `encode_vlq`, `decode_vlq`, `pv_encode`, and `pv_decode`.

## uvarint picture

Uvarint stores an unsigned integer in 7-bit groups. Each byte carries:

- low 7 bits: payload
- high bit: continuation flag

Groups are least-significant first.

```mermaid
flowchart LR
  N["n"] --> G["take low 7 bits"]
  G --> S["n >>= 7"]
  S --> M{"more groups?"}
  M -->|yes| B1["emit group | 0x80"]
  B1 --> G
  M -->|no| B2["emit group"]
```

Worked vectors:

| value        | bytes             | reason                  |
| ------------ | ----------------- | ----------------------- |
| 0            | `00`              | loop must emit once     |
| 127          | `7f`              | last 1-byte value       |
| 128          | `80 01`           | first 2-byte value      |
| 300          | `ac 02`           | `0x2c + (0x02 << 7)`    |
| 16384        | `80 80 01`        | bit 14 lands in group 2 |
| $2^{64} - 1$ | `ff` x9 then `01` | 10 bytes                |

Length:

$$
\max(1, \lceil \operatorname{bitlen}(n) / 7 \rceil)
$$

So a u64 takes at most 10 bytes. On the 10th byte, only bit 0 may be payload; any higher payload bit is overflow.

## encode recipe

The loop must be do-while shaped because `0` encodes as one byte.

<div class="notebook-runtime" data-notebook-runtime="notebook-runtime-dfyzph"></div>

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-1" id="code-cell-1" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-1" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-1" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-1">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-1" aria-label="Run code-cell-1" title="Run code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-1" aria-label="Edit code-cell-1" title="Edit code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-1" aria-label="Save code-cell-1 locally" title="Save code-cell-1 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-1" aria-label="Revert code-cell-1 local edit" title="Revert code-cell-1 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-1" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-1" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-1" hidden></div>

```python shell
def encode_uvarint(n: int) -> bytes:
  out = bytearray()
  while True:
    group = n & 0x7F
    n >>= 7
    if n:
      out.append(group | 0x80)
    else:
      out.append(group)
      return bytes(out)
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-1" hidden></div>

</div>

For this kit, `n` must be in `[0, 2**64)`. Reject negatives. Otherwise Python right-shifts a negative toward `-1`, and `while n` can run forever.

## decode recipe

State:

- `acc`
- `shift`
- `count`

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-2" id="code-cell-2" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-2" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-2" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-2">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-2" aria-label="Run code-cell-2" title="Run code-cell-2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-2" aria-label="Edit code-cell-2" title="Edit code-cell-2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-2" aria-label="Save code-cell-2 locally" title="Save code-cell-2 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-2" aria-label="Revert code-cell-2 local edit" title="Revert code-cell-2 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-2" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-2" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-2" hidden></div>

```python shell
acc = 0
shift = 0
count = 0
while True:
  b = read_byte()
  count += 1
  acc |= (b & 0x7F) << shift
  if b < 0x80:
    return acc, count
  shift += 7
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-2" hidden></div>

</div>

Trace for `ac 02`:

| byte | payload | shift | acc |
| ---- | ------- | ----- | --- |
| `ac` | `2c`    | 0     | 44  |
| `02` | `02`    | 7     | 300 |

C follow-up: cast before shifting.

```c
value |= (uint64_t)(b & 0x7f) << shift;
```

Without the cast, `(b & 0x7f)` is promoted to `int`, and shifting past 31 is undefined.

## error taxonomy

Keep these distinct. This is where interviewers grade actual protocol judgment.

| class     | example                           | recoverable?      | meaning                         |
| --------- | --------------------------------- | ----------------- | ------------------------------- |
| truncated | `80`                              | yes, in streaming | valid prefix, needs more bytes  |
| too long  | `80` x10 then `01`                | no                | u64 can never need an 11th byte |
| overflow  | 10th byte has payload above bit 0 | no                | value is `>= 2**64`             |
| overlong  | `80 00`                           | policy            | shorter encoding exists         |

Canonicality matters when bytes are hashed, signed, cached, or consensus-critical. Protobuf accepts overlong varints, so canonicality is not automatic. State the policy.

## zigzag

Raw two’s-complement negative numbers are terrible varints: protobuf `int64 -1` becomes `2**64 - 1`, which takes 10 bytes.

Zigzag maps small signed magnitudes to small unsigned values:

| n            | zigzag       |
| ------------ | ------------ |
| 0            | 0            |
| -1           | 1            |
| 1            | 2            |
| -2           | 3            |
| 2            | 4            |
| $2^{63} - 1$ | $2^{64} - 2$ |
| $-2^{63}$    | $2^{64} - 1$ |

Python:

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-3" id="code-cell-3" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-3" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-3" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-3">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-3" aria-label="Run code-cell-3" title="Run code-cell-3"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-3" aria-label="Edit code-cell-3" title="Edit code-cell-3"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-3" aria-label="Save code-cell-3 locally" title="Save code-cell-3 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-3" aria-label="Revert code-cell-3 local edit" title="Revert code-cell-3 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-3" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-3" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-3" hidden></div>

```python shell
MASK64 = (1 << 64) - 1


def zigzag_encode(n: int) -> int:
  return ((n << 1) ^ (n >> 63)) & MASK64


def zigzag_decode(u: int) -> int:
  return (u >> 1) ^ -(u & 1)
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-3" hidden></div>

</div>

The encode mask is mandatory in Python because negative integers have infinitely many sign bits.

## families

| format            | group order                 | length signal             | why care                         |
| ----------------- | --------------------------- | ------------------------- | -------------------------------- |
| LEB128 / protobuf | least-significant first     | continuation bit per byte | likely prompt                    |
| MIDI VLQ          | most-significant first      | continuation bit per byte | decode accumulates left-to-right |
| UTF-8             | most-significant first      | lead byte prefix          | self-synchronizing               |
| PrefixVarint      | lead byte says total length | first-byte tag            | fewer branches                   |
| SQLite varint     | most-significant first      | continuation, 9-byte cap  | byte 9 carries 8 payload bits    |
| Group Varint      | four values per tag byte    | 2 tag bits/value          | bulk u32 throughput              |

Use LEB128 unless the prompt names another format.

## guards

- Negative input to unsigned encode must raise.
- Empty input is truncated, not value 0.
- `0` must emit `00`.
- A length prefix must be checked against `max_frame` before allocation.
- Return convention matters: `(value, consumed)` and `(value, new_offset)` are not interchangeable.
- “Little-endian” in LEB128 means 7-bit group order, not CPU byte order.

## drills

1. Encode 150.
2. Decode `e5 8e 26`.
3. Classify `80`, `80 00`, and `80` x10 then `01`.
4. Explain why `ff 00` is overlong.
5. Compute zigzag of `-3`.
6. Say why protobuf `int64 -1` takes 10 bytes.
7. Give the max encoded length of u32 and u64.
8. Explain the missing-cast C bug.

<script type="application/json" data-notebook-runtime-data>{"id":"notebook-runtime-dfyzph","sourcePath":"hinterland/prep/bt/03-varint/notes.md","language":"python","indexUrl":"https://cdn.jsdelivr.net/pyodide/v0.29.4/full/","cells":[{"id":"code-cell-1","source":"def encode_uvarint(n: int) -\u003e bytes:\n  out = bytearray()\n  while True:\n    group = n \u0026 0x7F\n    n \u003e\u003e= 7\n    if n:\n      out.append(group | 0x80)\n    else:\n      out.append(group)\n      return bytes(out)","language":"python","executionIndex":null},{"id":"code-cell-2","source":"acc = 0\nshift = 0\ncount = 0\nwhile True:\n  b = read_byte()\n  count += 1\n  acc |= (b \u0026 0x7F) \u003c\u003c shift\n  if b \u003c 0x80:\n    return acc, count\n  shift += 7","language":"python","executionIndex":null},{"id":"code-cell-3","source":"MASK64 = (1 \u003c\u003c 64) - 1\n\n\ndef zigzag_encode(n: int) -\u003e int:\n  return ((n \u003c\u003c 1) ^ (n \u003e\u003e 63)) \u0026 MASK64\n\n\ndef zigzag_decode(u: int) -\u003e int:\n  return (u \u003e\u003e 1) ^ -(u \u0026 1)","language":"python","executionIndex":null}],"toolbar":false,"debug":true,"vimMode":true}</script>

