Decorator & Memo¶
How cross-argument checking works¶
A dimension memo maps names such as N and C to the concrete sizes seen during one call. That shared memo is why:
x: F32[N, C]can bindN = 4y: F32[N, C]is then required to use the sameN = 4- the return value can be checked against the same bindings
from beartype import beartype
from shapix import N, C
from shapix.numpy import F32
@beartype
def f(x: F32[N, C], y: F32[N, C]) -> F32[N, C]:
...
Plain @beartype is the normal entry point. Most users never need more than that.
Which decorator should you choose?¶
Use plain @beartype by default.
Add @shapix.check only when you need explicit memo scope rather than frame discovery.
| Choice | What it does | When it is the right fit |
|---|---|---|
@beartype |
lets shapix find the shared memo by walking the beartype call stack | normal application code with no unusual wrapper layers |
@shapix.check + @beartype |
pushes one memo explicitly before the call and pops it after | middleware-heavy stacks, wrapper decorators, async Value(...), or defensive correctness |
@shapix.check(conf=...) |
same explicit memo handling, and applies beartype for you | when you also want to pass BeartypeConf without stacking both decorators manually |
What @shapix.check changes:
- how the shared memo is found
- how long that memo lives around the call
What it does not change:
- the array, dtype, or shape semantics themselves
- whether return values are checked
- whether plain
@beartyperemains the simplest choice for ordinary code
@shapix.check¶
@shapix.check provides explicit memo management. Instead of discovering the correct beartype frame dynamically, it pushes a memo before the call and pops it afterwards.
Usage mode 1: memo only¶
import shapix
from beartype import beartype
from shapix import N, C
from shapix.numpy import F32
@shapix.check
@beartype
def f(x: F32[N, C], y: F32[N, C]) -> F32[N, C]:
...
Usage mode 2: memo + beartype combined¶
import shapix
from beartype import BeartypeConf, BeartypeStrategy
from shapix import N, C
from shapix.numpy import F32
@shapix.check(conf=BeartypeConf(strategy=BeartypeStrategy.On))
def f(x: F32[N, C], y: F32[N, C]) -> F32[N, C]:
...
When do you need it?¶
Use it when you want memo scope to stop depending on call-stack shape.
Typical reasons:
- extra decorators or framework wrappers add their own call frames
Value(...)needs an explicit scope across async execution- you want
BeartypeConfand memo handling from one decorator - you want a defensive guarantee that exotic stack layouts will not matter
Concrete examples:
- web frameworks or middleware that wrap handlers before beartype sees them
- utility decorators around model code that add their own frames
- async code where a
Value("size")expression should keep using the original bound scope until the coroutine finishes - tests or runtime environments where you do not want frame-layout assumptions to be part of correctness
Async support¶
@shapix.check supports both sync and async callables:
inspect.iscoroutinefunction()remainsTrue- the memo lifetime covers the awaited execution, not just coroutine creation
- parameter mismatches and return mismatches are still raised normally
@shapix.check(conf=...)works for async functions too
Generator functions are intentionally rejected:
- sync generators raise
TypeError - async generators raise
TypeError
When you don't need it
If plain @beartype is already working in your codebase, keep it simple. @shapix.check is an explicit escape hatch, not the default style.
check_context¶
For manual is_bearable() checks, use check_context so multiple validations share one memo.
from beartype.door import is_bearable
import shapix
from shapix import N, C
from shapix.numpy import F32
is_bearable(x, F32[N, C]) # independent temporary memo
is_bearable(y, F32[N, C]) # independent temporary memo
with shapix.check_context():
assert is_bearable(x, F32[N, C])
assert is_bearable(y, F32[N, C])
check_context supports both:
with shapix.check_context():async with shapix.check_context():
Thread and async safety¶
- frame-based auto-detection uses
threading.local()for thread isolation - the explicit memo stack used by
@shapix.checkandcheck_context()usescontextvars.ContextVar
Note
Child tasks inheriting an active parent context share the same live memo by reference. For full task isolation, each task should enter its own check_context().
from __future__ import annotations¶
Shapix works with from __future__ import annotations, but every symbol used inside the annotation must still be imported in module scope:
from __future__ import annotations
from beartype import beartype
from shapix import B, C
from shapix.numpy import F32
@beartype
def f(x: F32[~B, C]): # type: ignore[valid-type]
...
The same rule applies to custom dimensions and structure symbols: if the annotation refers to a runtime object, that symbol must exist in module scope when beartype resolves the annotation.