Skip to content

espetro/refined

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

refined-py

Near-zero-overhead refinement types for Python — runtime contracts via Annotated, lightweight enforcement powered by beartype, optional compile-to-inline guards via libcst, and a small mypy plugin for static ergonomics.


Installation

# runtime only (annotated-types + beartype)
pip install -e ".[runtime]"

# full dev suite (includes codegen, mypy, pytest, ruff)
pip install -e ".[dev]"

Or with uv:

uv pip install -e ".[dev]"

Quick Start

Layer 1 — Predicate vocabulary

from refined_py import Gt, Ge, Lt, Le, MinLen, MaxLen, Interval, Matches, And, Or, Not

All predicates are frozen=True, slots=True dataclasses and compatible with annotated-types where they overlap.

Layer 2 — Runtime enforcement (@refine)

from typing import Annotated
from refined_py import Gt, refine

PositiveInt = Annotated[int, Gt(0)]

@refine
def double(x: PositiveInt) -> int:
    return x * 2

double(5)   # OK
double(-1)  # raises beartype exception at call time

@refine is a no-op when Python runs with -O (i.e. __debug__ == False), so there is zero overhead in optimized/ production builds.

@refine_dataclass

from dataclasses import dataclass
from typing import Annotated
from refined_py import MinLen, Ge, refine_dataclass

@refine_dataclass
@dataclass
class User:
    name: Annotated[str, MinLen(1)]
    age: Annotated[int, Ge(0)]

User("Alice", 30)  # OK
User("", 30)       # raises on __init__

Layer 3 — Codegen CLI (zero-dependency output)

Transform decorated functions into flat inline isinstance guards:

python -m refined_py.codegen examples/codegen_target.py --dry-run
python -m refined_py.codegen examples/codegen_target.py        # in-place

The generated file contains no refined_py imports — just plain Python guards, suitable for environments where you cannot add a runtime dependency.

Layer 4 — Mypy plugin

A minimal mypy plugin ships in refined_py.mypy_plugin. It expands Refined[T, Predicate] to T during type analysis so refinement annotations are erased statically while still being usable as subtypes of the base type.

from refined_py import Refined, Gt

def take_positive(x: Refined[int, Gt(0)]) -> int:
    return x

take_positive(5)      # OK (mypy)
take_positive("hi")   # mypy error: str is not int

Enable it in pyproject.toml:

[tool.mypy]
plugins = ["refined_py.mypy_plugin"]

Note: Annotated[T, Predicate] is already treated as T by mypy natively. The plugin mainly improves ergonomics for the Refined alias.


Architecture (4 layers)

┌──────────────────────────────────────────────────────────┐
│ Layer 4  mypy_plugin.py   ── static ergonomics          │
├──────────────────────────────────────────────────────────┤
│ Layer 3  codegen.py       ── compile to inline guards   │
├──────────────────────────────────────────────────────────┤
│ Layer 2  enforce.py       ── @refine / @refine_dataclass│
├──────────────────────────────────────────────────────────┤
│ Layer 1  core.py          ── predicate dataclasses      │
└──────────────────────────────────────────────────────────┘

Milestones

Milestone Status Commit
0 — Foundation 4a83bfe
1 — Predicates + @refine 41aa562
2 — @refine_dataclass aafa2ee
3 — Codegen CLI e9f15d1
4 — Mypy plugin 0d31337

Development

# run tests
pytest tests/ -v

# run formatter / linter
./scripts/validate

About

Near-zero-overhead refinement types for Python

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors