Small, typed-enough Python syntax crimes that form one deliberately coherent ecosystem with typed-errs: pipe values, defer cleanup, and match rich values without falling back to nullable match results.
uv add python-crimesRunnable examples live in examples: pipes, deferred
cleanup, structural matching and typed-errs variants, plus reusable dispatch.
python_crimes/
├── pipe.py @pipe and value @ function
├── defer.py DeferStack, with defer(), @deferred, terminate()
├── patterns.py reusable and composable patterns
├── match.py bound matching and reusable Dispatch
└── typed.py Result and Option patterns backed by typed-errs
@pipe preserves normal calls while allowing the left-to-right form.
from python_crimes import pipe
@pipe
def parse(raw: str) -> dict[str, object]: ...
@pipe
def validate(config: dict[str, object]) -> Config: ...
config = raw @ parse @ validateFor a function with configuration after its piped first argument, use the
explicit partial form: score @ clamp.with_(lo=0, hi=100).
DeferStack is the boring LIFO implementation. defer and @deferred are
just convenient frontends over it.
from python_crimes import deferred, terminate
from typed_errs import Result, catch_bubble
@catch_bubble
@deferred
def write_report(d, path) -> Result[None, WriteError]:
writer = write_text(path).q
d << terminate(writer)
writer("started\\n")
return Ok(None)The cleanup still runs when .q bubbles an Err, because normal Python stack
unwinding reaches the deferred scope first.
Patterns return typed_errs.Option[Match]: successful matching is
Some(Match(...)), and failure is Nothing(). The matcher itself therefore
does not use Match | None as an internal failure protocol.
from python_crimes import capture, ge, gt, match_, type_
with match_(data) as m:
m.case(200, 201, 204) << "success"
m.case(type_(int)).when(gt(0)) << (lambda number: number * 2)
m.regex(r"(\d+)x(\d+)") << (lambda width, height: (int(width), int(height)))
m.case(
{
"type": "user",
"payload": {"name": capture(str), "age": capture(int)},
}
) << (lambda name, age: (name, age))
m.default << "unknown"
result = m.valueThe context manager is convenient for a visually large decision tree. For a
small match inside an expression or function return, use the exact same engine
without with:
level = (
match_(raw_level)
.case(type_(int))
.when(gt(0))
.then(lambda value: value * 2)
.regex(r"level:(\d+)")
.then(int)
.default.then(0)
.value
)<< and .then(...) are equivalent. A callable result is a handler; use
const(callable_value) when a callable itself is the wanted result.
Patterns compose with &, |, and ~; helpers include eq, type_, when,
gt/ge/lt/le, in_, contains, is_, regex, attr, length, ANY,
and REST. Lists and mappings are structural patterns recursively, and
capture(...) passes values to the selected handler in traversal order.
Result and Option variants have dedicated arms with payload capture:
with match_(read(path)) as m:
m.ok << process
m.err << report
with match_(find_user()) as m:
m.some << (lambda user: user.name)
m.nothing << "anonymous"No optional adapter is needed: python-crimes depends on typed-errs and
ships these patterns as part of its public API.
The special arms deliberately unwrap only when their variant matched:
Ok(value) -> m.ok handler(value)
Err(error) -> m.err handler(error)
Some(value) -> m.some handler(value)
Nothing() -> m.nothing constant or handler(subject)
For ordinary structural matching, failed patterns are Nothing() and
successful patterns are Some(Match(captures)); this is the same explicit
absence vocabulary used everywhere else in the ecosystem.
from python_crimes import ge, matcher, type_
status = (
matcher()
.case(200)
.then("ok")
.case(404)
.then("missing")
.case(type_(int) & ge(500))
.then("server error")
.default.then("unknown")
)
message = 503 @ status- typed-errs supplies
Result,Option, and.qbubbling. - typed-file-io supplies typed file
callbacks; pair a callback writer with
deferandterminate. - sqlite-callback-store supplies short-lived typed SQLite callbacks.
typed-errs
This is a personal library, but it is not private or locked to my projects. You may use it in general Python work and in 42 projects under the MIT license; just follow the rules that apply to your campus and assignment.
Contributions are welcome: open an issue or send a pull request. I do not care whether a contribution is written by hand, AI-assisted, or generated another way; I care about whether it is correct, tested, understandable, and a good fit. Because this is opinionated personal infrastructure, pull requests are reviewed selectively and are likely to be rejected unless they clearly improve the library without making it harder to maintain.
Run mise run check. Every push to master publishes a unique 0.0.<CI run> ZeroVer
version through PyPI Trusted Publishing. mise run publish remains available.