Sized integer types: exhaustive Literal unions statically, Annotated bounds at runtime
Project description
types-bits
u1..u10 and i1..i10 as fully materialized Literal unions. Generated .pyi, zero
runtime cost, exhaustively checked.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from types_bits import u4
x: u4 = 15 # ok
y: u4 = 16 # error, on every checker
camas runs the gate; camas --list enumerates it. CI fans the same tree out over a
runner axis, with both matrix axes emitted from tasks.py. camas bench and
camas bench_shapes regenerate everything below into bench/results/.
Runtime cost
Zero. The stub is a PEP 484 .pyi behind TYPE_CHECKING. import types_bits
loads two modules and no stdlib (tests/test_runtime.py). Every number below is
type-checker wall clock, per check run.
Check cost by width
Seconds, cold, best of one, Python 3.14.1 / WSL2. Encoding flat
(type uN = Literal[0, ..., 2**N-1]), declaring u1..uN (so a probe at N bits holds 2^(N+1)-2 literals) and widening each
into the next.
| bits | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|---|---|---|---|---|---|---|
| 8 | 2.30 | 0.68 | 0.71 | 0.08 | 0.32 | 0.10 |
| 10 | 1.98 | 0.75 | 0.83 | 0.09 | 0.38 | 0.12 |
| 12 | 2.22 | 0.79 | 0.85 | 0.11 | 0.31 | 0.33 |
| 14 | 3.10 | 0.59 | 0.64 | 0.17 | 0.27 | 3.23 |
| 16 | 3.85 | 0.70 | 0.79 | 0.41 | 0.46 | 51.26 |
Shipped library (u1..u10 + i1..i10): mypy 2.05, basedpyright 1.43,
pyright 1.13, pyrefly 0.24, zuban 0.08, ty 0.06 — matching the type uN = int control.
xychart-beta
title "16 bits"
x-axis [zuban, mypy, basedpyright, pyright, pyrefly, ty]
y-axis "seconds" 0 --> 55
bar [51.26, 3.85, 0.79, 0.70, 0.46, 0.41]
zuban, same sweep — roughly 16x per extra 2 bits past 12:
xychart-beta
title "zuban vs width"
x-axis "bits" [8, 10, 12, 14, 16]
y-axis "seconds" 0 --> 55
line [0.10, 0.12, 0.33, 3.23, 51.26]
The other five, 0–4s axis. Rising line is mypy; flat cluster is pyright, basedpyright, pyrefly, ty:
xychart-beta
title "mypy, pyright, basedpyright, pyrefly, ty"
x-axis "bits" [8, 10, 12, 14, 16]
y-axis "seconds" 0 --> 4
line [2.30, 1.98, 2.22, 3.10, 3.85]
line [0.68, 0.75, 0.79, 0.59, 0.70]
line [0.71, 0.83, 0.85, 0.64, 0.79]
line [0.32, 0.38, 0.31, 0.27, 0.46]
line [0.08, 0.09, 0.11, 0.17, 0.41]
Fixed vs marginal cost
u(N-1) and u(N) declared in every row; only the use count varies. Italic rows are the
type uN = int control.
10 bits — indistinguishable from int:
| uses | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|---|---|---|---|---|---|---|
| declared, unused | 1.64 | 0.55 | 0.60 | 0.05 | 0.23 | 0.10 |
| control | 1.64 | 0.60 | 0.59 | 0.06 | 0.23 | 0.08 |
| 1 widening | 1.50 | 0.56 | 0.64 | 0.06 | 0.26 | 0.09 |
| 10 widenings | 1.75 | 0.65 | 0.90 | 0.08 | 0.29 | 0.20 |
16 bits — 98,304 literals:
| uses | mypy | pyright | basedpyright | ty | pyrefly | zuban |
|---|---|---|---|---|---|---|
| declared, unused | 2.79 | 0.66 | 0.64 | 0.18 | 0.29 | 0.20 |
| control | 1.57 | 0.62 | 0.59 | 0.05 | 0.28 | 0.08 |
| 1 assignment | 2.77 | 0.62 | 0.65 | 0.19 | 0.33 | 0.21 |
| 1 widening | 2.81 | 0.64 | 0.65 | 0.22 | 0.37 | 35.66 |
| 10 widenings | 3.31 | 0.56 | 0.58 | 0.20 | 0.31 | timeout (>180s) |
- Declaration: fixed per check run, ~linear in literals (~12 µs/literal on mypy). Not per importing file, not per use.
- Assignment: free. Enumerated membership is a hash lookup.
- Widening: free except zuban at width.
Widening
Cost tracks the narrow operand, not the wide one. Wide side fixed at u16, one widening:
| narrow | zuban | mypy | ty |
|---|---|---|---|
| u1 | 0.18 | 2.54 | 0.20 |
| u4 | 0.18 | 2.46 | 0.15 |
| u8 | 0.16 | 2.67 | 0.21 |
| u12 | 0.76 | 2.50 | 0.17 |
| u15 | 35.21 | 2.80 | 0.18 |
Both operands must be large. At the 10-bit ceiling the worst case (u9 → u10) is 0.09s.
Annotating a boundary at one width, so callers assign literals rather than widen between
adjacent wide aliases, avoids the shape entirely.
Encodings
| encoding | form | result |
|---|---|---|
flat |
Literal[0, ..., 2**N-1] |
shipped; fastest, portable |
nested |
Literal[u9, 512, ...] |
legal per PEP 586; pyrefly rejects past ~10 levels (Invalid type inside literal, int, 1 error at 10 bits → 7 at 16), ty at 16 |
union |
u9 | Literal[512, ...] |
ty 44.24s at 14 bits vs 0.17s flat |
annotated |
Annotated[Literal[...], Ge, Le] |
tracks flat within noise to 14 bits |
opaque |
int |
control |
xychart-beta
title "ty at 14 bits by encoding"
x-axis [union, nested, annotated, flat]
y-axis "seconds" 0 --> 46
bar [44.24, 0.23, 0.22, 0.17]
Runtime tier
PEP 562 module __getattr__ resolves the same names to
Annotated[int, Ge(lo), Le(hi)] (PEP 593), the shape
annotated-types consumers read. tests/test_runtime.py pins it against the static
bounds for all 20 widths. Needs the rt extra; annotated_types imports on first
attribute access.
from pydantic import TypeAdapter
from types_bits import u8 # Annotated[int, Ge(0), Le(255)] at runtime
TypeAdapter(u8).validate_python(256) # ValidationError
Prior art
range-typed-integers defines u8 = NewType('u8', Annotated[int, ValueRange(0, 255)])
for the byte widths u8..u64 / i8..i64.
| range-typed-integers | types-bits | |
|---|---|---|
| carrier | NewType over Annotated[int, ValueRange] |
Literal enumeration |
| bound enforced by | runtime u8_checked() / check_int(), raising IntegerBoundError |
the type checker |
a: u8 = 12 |
mypy error — int is not u8; requires u8(12) |
ok |
a: u8 = 900 |
mypy error, identical to the line above | error, and distinguished |
u8(900) |
accepted statically | n/a |
| widths | u8..u64 | u1..u10 |
Verified against every checker in the gate: mypy reports the same Incompatible types in assignment (expression has type "int", variable has type "u8") on the in-range and out-of-range
lines alike. The range is metadata no checker reads (PEP 746 would not change
this). ValueRange is O(1) per type, so it reaches u64; enumeration is O(2^N), so it
stops at u10.
PEPs
No PEP provides bounded integers.
| PEP | Status | Relevance |
|---|---|---|
| 586 – Literal Types | Final | The mechanism. Calls Literal insufficient for numpy-style numeric code and defers integer generics. Permits the nested form pyrefly rejects. |
593 – Annotated |
Final | Metadata channel for the runtime tier. |
| 695 – Type Parameter Syntax | Final | type uN = ... in the stub. |
| 561 – Packaging Type Information | Final | py.typed; why the stub ships in the wheel. |
562 – Module __getattr__ |
Final | One name, two tiers. |
| 649 / 749 – Deferred Annotations | Final (3.14) | Guarded import gets cheaper. |
746 – Type checking Annotated metadata |
Draft, targets 3.15 | Lets a checker verify metadata suits its type. Does not make any checker enforce Ge/Le. |
- python/typing#554, "Support for Range Types?" — closed. Requested an Ada-style
RangeType. - discuss.python.org, "Use type hinting with bound constraints, e.g.
int[0:15]" — no resolution. Objections: most constraints are not statically validatable; ranges need new type-system machinery; arithmetic is not closed. Landed onAnnotated+annotated-typeswith runtime validation.
fixtures/reject.py covers the arithmetic case: every checker rejects u4 + u4 where a
u4 is required. mypy widens the sum to int; ty and zuban widen it to Literal[0, ..., 30].
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file types_bits-0.1.0.tar.gz.
File metadata
- Download URL: types_bits-0.1.0.tar.gz
- Upload date:
- Size: 86.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e6d88d2df3b582a3fc3c35e6718a5451e440a55f1b1633a61ef2da70bab6e4a
|
|
| MD5 |
cec032114b86c3b188d1a3d5190d21f7
|
|
| BLAKE2b-256 |
8fe6b1855178ac07dfe9d46a459099ec4c636d51b581284ccd2e5f9b91031222
|
File details
Details for the file types_bits-0.1.0-py3-none-any.whl.
File metadata
- Download URL: types_bits-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf67b0f4f31ea7250bfaa262c88ca1d6c102fad14d593d74b4d4d091865be084
|
|
| MD5 |
6847f2a58007f57c5aa5965058487e2a
|
|
| BLAKE2b-256 |
59ec032285e8e7495b98a4de9f29f20a4a697bcb029c23f7069657c0479b3e5a
|