Skip to main content

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 (u9u10) 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.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

types_bits-0.1.0.tar.gz (86.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

types_bits-0.1.0-py3-none-any.whl (13.3 kB view details)

Uploaded Python 3

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

Hashes for types_bits-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1e6d88d2df3b582a3fc3c35e6718a5451e440a55f1b1633a61ef2da70bab6e4a
MD5 cec032114b86c3b188d1a3d5190d21f7
BLAKE2b-256 8fe6b1855178ac07dfe9d46a459099ec4c636d51b581284ccd2e5f9b91031222

See more details on using hashes here.

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

Hashes for types_bits-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cf67b0f4f31ea7250bfaa262c88ca1d6c102fad14d593d74b4d4d091865be084
MD5 6847f2a58007f57c5aa5965058487e2a
BLAKE2b-256 59ec032285e8e7495b98a4de9f29f20a4a697bcb029c23f7069657c0479b3e5a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page