Skip to main content

nodrill

PyPI version PyPI Supported Python Versions CI codecov

nodrill gives a call tree a shared, scoped context. Values set in a provider block are visible to any function below it through use(), without being passed through the signatures in between. It is built on contextvars, so lookups are thread-safe and asyncio-task-safe, and it has no dependencies.

from dataclasses import dataclass

from nodrill import provider, use


@dataclass
class RequestScope:
    user_id: int
    db: str


def handle_request():
    with provider(RequestScope(user_id=42, db="postgres://...")):
        render_page()


def render_page():
    return render_sidebar()  # knows nothing about RequestScope


def render_sidebar():
    scope = use(RequestScope)  # inferred as RequestScope
    return f"{scope.user_id} @ {scope.db}"

String names work too, when a typed key is more than the case needs.

with provider("app", db=engine) as ctx:
    ctx.user_id = 42
    handle()  # any callee reads use("app").db

What you get

  • Typed keys: use(Config) is inferred as Config, under both mypy and pyright.
  • String namespaces for the values that do not deserve a class, as above.
  • @inject to declare the dependency in the signature and still pass it explicitly in a test.
  • set_default(Config, factory) for code that has to run outside any provider.
  • Threads and asyncio: tasks inherit the context, wrap and Executor carry it into threads.
  • frozen=True hands consumers a read-only view while the block keeps a writable object.
  • extend=True for a scope that accumulates as the call descends, one layer per with block, each unwound on exit.
  • lazy(Cls, factory) for a value that costs something to build and only some requests read.
  • ref("myapp.context:RequestScope") for a key that lives in a module you cannot import, because it imports yours.
  • isolate() to give a test fresh context state and roll everything back after it.
  • No dependencies, Python 3.10 and up, and a public API of nineteen names.

Cost

A lookup is one dict read on a single ContextVar, and nothing is constructed, resolved or cached along the way. The first five rows are one function doing one read, reached five ways, so they can be read against each other and against the parameter they replace.

operation ns ×
one read in a function, value passed in as a parameter 23 1.0
the same read through use() 64 2.8
the same read through @inject 72 3.1
the same read through a frozen=True provider 120 5.3
the same read through a resolved lazy provider 144 6.3
use(Config) on its own, without the call frame 45 2.0
the same lookup through a ref() key 143 6.3
bare ContextVar.get(), for reference 16 0.7
with provider(...), enter and exit 563 25
the same with 8 providers already open 652 28
with provider(lazy(...)), entered and exited unread 1536 67
with provider(..., extend=True), over an 8-attribute namespace 1706 74
wrap(fn)(), per call into a thread 536 23

CPython 3.14.5, arm64.

The × column is against handing the value in as a parameter, which is the alternative nodrill removes from the signatures in between. Reading through use() costs a little over the parameter it replaces; @inject costs more, because it fills the argument before the body runs; frozen=True and lazy add a proxy hop to every attribute the consumer touches. A ref() key pays for a Python-level hash and one equality check where a class hashes in C, on the lookups that go through a ref and on no others. A request that reads a provided value a hundred times spends microseconds in nodrill, against hundreds of microseconds for one round trip to a database.

Entering a provider is the expensive end, because it copies the registry so that sibling tasks stay isolated. That copy is proportional to how many providers are open, which the with provider(...) rows price at one and at eight, and it happens once per scope rather than once per lookup. A lazy provider pays for the cell it allocates on top, which is the trade the feature is for: a microsecond on entry, against a value that is never built at all on the requests that never read it. An extending layer copies the enclosing namespace on top of the registry, so its row grows with how many attributes have accumulated rather than with how many layers are open, and that second copy is what keeps a sibling task from seeing a layer opened after it started.

The absolute numbers move with the machine, and the ratios are the part worth reading. Regenerate with make bench ARGS=--write, which measures on your machine and rewrites the block above. A rerun lands within a few percent, so read the digits as approximate; nothing here runs in CI, because timing on a shared runner measures the runner.

Install

pip install nodrill

Python 3.10 or newer.

Documentation

Full documentation is at https://nodrill.readthedocs.io/.

Start with the tutorial, which covers the whole library in about ten minutes, over typed class keys, fallbacks for a miss, @inject, threads and asyncio, frozen providers, and testing.

Contributing

Bug reports and small focused pull requests are welcome. See CONTRIBUTING.md. make install sets up the environment, and make runs the same gate CI does.

Security issues go through a private advisory rather than the issue tracker.

License

MIT

Release files for nodrill 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nodrill 0.2.0
File Size Uploaded
nodrill-0.2.0.tar.gz 58.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nodrill 0.2.0
File Interpreter ABI Platform
nodrill-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.8 kB

Release files / nodrill-0.2.0.tar.gz

Download URL nodrill-0.2.0.tar.gz
Size 58.9 kB
Tags Source
SHA-256 checksum
How to use checksums
7ba9c639801b9e594e6816c41e02393fa8e93e64a246c4a66a7e1e9ce6ab0fa9
BLAKE2b-256 checksum
How to use checksums
c91906a4c617e2f094a17d0fdb1eaaa785a5b4ef3210a862e3e7955b7fa1251b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release files / nodrill-0.2.0-py3-none-any.whl

Download URL nodrill-0.2.0-py3-none-any.whl
Size 32.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c78dc4e6063dce2e232245dc4d655c92dbd5d08ea6be2c7a56e60f048901f4a3
BLAKE2b-256 checksum
How to use checksums
8329c86f4554ce76d7aaba463e376e2cd7ab795c2ebccd4449703c40b7beec98
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.0

2 release files

0.3.0

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page