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

The rest of the library

A key is a class or a string, and use(Config) is inferred as Config under both mypy and pyright. provider(instance, key=Repository) registers under a protocol rather than under the class that happens to implement it, and ref("myapp.context:Scope") names a key that lives in a module importing yours.

How the value is handed out is the provider's decision. lazy(Cls, factory) builds it on the first read, and not at all without one. frozen=True gives consumers a read-only view while the block keeps writing, and extend=True lets a scope accumulate as the call descends, one layer per block, each unwound on exit. sealed=True makes a value that escaped its block say so where it is touched, naming the line the block opened and the line that used it, instead of failing later as a closed connection.

Reading it is use() anywhere below, or @inject to put the dependency in the signature where a test can still pass it explicitly. Tasks inherit the context, wrap and Executor carry it into threads, and set_default(Config, factory) answers a read that ran outside every provider. Past the edge of the process, where nothing can be carried by reference, export("trace") renders the providers you name as a plain dict that JSON holds and adopt(payload) opens them again in the worker.

When it goes wrong, debug() turns a miss into a diagnosis naming the thread, the task and the line the provider is open on, and annotate_exceptions() puts the scope on the traceback. isolate() gives a test fresh context state and rolls it back afterwards. Everything above is importable from the top-level package, and there is nothing else to import.

Overhead

A lookup is one dict read on a single ContextVar, and nothing is constructed, resolved or cached along the way. Reading through use() costs a little over the parameter it replaces, and @inject a little more than that, because it fills the argument before the body runs. frozen=True, lazy and sealed=True add a proxy hop to every attribute the consumer touches, and a ref() key pays a Python-level hash where a class hashes in C. 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, and it happens once per scope rather than once per lookup. An extending layer copies the enclosing namespace on top of that, which is what keeps a sibling task from seeing a layer opened after it started.

The numbers, the machine that produced them and the script that timed them are on the performance page, and make bench ARGS=--write runs it on yours.

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 core in about ten minutes, over typed class keys, fallbacks for a miss, @inject, threads and asyncio, 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.4.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.4.0
File Size Uploaded
nodrill-0.4.0.tar.gz 106.0 kB Details

Built distribution (wheel)

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

Total release size: 160.9 kB

Release files / nodrill-0.4.0.tar.gz

Download URL nodrill-0.4.0.tar.gz
Size 106.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c5bbf6782fa4e6ddb2dae849f83eeba4b7f8bc75104db2c0c355db4396c37edd
BLAKE2b-256 checksum
How to use checksums
f9a382f1bc9b93475538f063af58bb1ccc5a5fbb7d85b726a1bcd18edcdd5849
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 22, 2026.

Transparency log

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

Download URL nodrill-0.4.0-py3-none-any.whl
Size 54.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
92763fe5c00b994e099c1891f62a1b2e8414bf0de413190ec644fd3ff3880987
BLAKE2b-256 checksum
How to use checksums
85dc0a5fa7763c21300140ff62084eb6d2dac6423554cb84ec7d41a0868687d4
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 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

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