Skip to main content

Idunn

Idunn

Idunn is a tiny Python dependency-inversion toolkit built around constructor-time injection only: three decorators, one discovery call, nothing to configure.

"Everything should be made as simple as possible, but not simpler." — Albert Einstein

The name comes from Iðunn, the Norse keeper of the apples that keep the gods young. A port is an empty basket, an adapter is the apple you put in it, and a consumer just asks for the basket.

This file is the everyday reference. Read About Idunn for the thinking behind it and Advanced for environments, keys, test isolation, and the engine.


Idunn

Introduction

Installation

pip install idunn        # or: poetry add idunn

idunn (lowercase) is the package; Idunn (the class) is the one process-wide container inside it. Idunn() always returns that same shared instance.

Importing

from idunn import Port, Adapter, Invert  # the three decorators
from idunn import Idunn  # the container: autodiscover() or bootstrap(), once
from idunn import LifecycleEnum  # TRANSIENT (default) or SINGLETON, passed to @Adapter
from idunn import IdunnError  # base of the exception hierarchy
from idunn import ReportMap  # the TypedDict autodiscover() and bootstrap() return

That is the entire surface. Registration, selection, and construction happen behind the container.


Idunn

Discovery: autodiscover and bootstrap

Idunn registers nothing on import. Decorators only attach metadata; one discovery call per process imports your modules so the decorated classes get catalogued. Both calls import every module under root_package (skipping __main__ and test modules) and register each class wearing @Port or @Adapter, whatever its module is called. The package you name is the boundary: nothing outside it is imported or registered. Ports register before adapters, undecorated classes are ignored, and nothing is constructed.

Discovery imports everything under the root, so keep module import cheap and side-effect free. Put scripts behind if __name__ == '__main__':, and pass exclude= for modules that do work at import or need optional dependencies.

Call Use it when Behavior
Idunn().autodiscover('my_app') You own the process: an application with a main(), an app factory, or a startup hook. Runs discovery each time it is called. Call it once at startup, before the first @Invert() object is built.
Idunn().bootstrap('my_lib') You ship a library and have no startup hook to call autodiscover from. autodiscover guarded to run once per root. Call it from every public entry point: the first call discovers, later calls return the cached report. Thread-safe.

Applications

from idunn import Idunn

Idunn().autodiscover('my_app')  # once, at startup
app = MyApp()  # MyApp.__init__ is @Invert()-decorated; the graph wires itself

Libraries

A library is imported, not started, and discovery at import time is a side effect nobody asked for. Put bootstrap inside the library's own entry points so wiring happens the first time anyone uses it, and bootstrap the narrowest package that holds your ports and adapters:

from idunn import Idunn


class Engine:
  """Example engine class"""

  ...


class MyLib:
  """Public entry point; wires my_lib on first use."""

  def __init__(self) -> None:
    Idunn().bootstrap('my_lib')  # discovers once; a cheap no-op afterwards
    self._engine = Engine()  # an @Invert()-decorated worker

Both calls return a ReportMap listing the modules imported and the classes registered, handy for a startup log. Discovery accumulates, so an application calls autodiscover for its own package and any library it uses still bootstraps itself.


Idunn

Normal usage

Discovery finds every @Port and @Adapter under the root you name, whatever the modules are called. For a small app, the layout below is all you need: a ports.py, an adapters.py, and your entry point beside them. As it grows, turn either file into a package (ports/, adapters/stripe.py) or give each feature its own pair (billing/ports.py, billing/adapters.py). The names are for the people reading your code; Idunn doesn't check them. Decorate the consumer's constructor with @Invert(), discover once, then construct.

my_app/
  __init__.py
  main.py
  ports.py
  adapters.py

See examples in /examples/my_app

# my_app/ports.py
from typing import Protocol

from idunn import Port


@Port
class AppleBasketPort(Protocol):
  def take_apple(self) -> str: ...
# my_app/adapters.py
from idunn import Adapter, LifecycleEnum

from .ports import AppleBasketPort


@Adapter(AppleBasketPort, lifecycle=LifecycleEnum.SINGLETON)
class GoldenAppleBasketAdapter(AppleBasketPort):
  def take_apple(self) -> str:
    return 'youth restored'
# my_app/main.py
from idunn import Idunn, Invert

from .ports import AppleBasketPort


class Feast:
  basket: AppleBasketPort  # declared for the type checker; @Invert assigns it

  @Invert()
  def __init__(self, basket: AppleBasketPort, host: str) -> None:
    self.host = host  # self.basket is injected and assigned for you

  def serve(self) -> str:
    return f'{self.host}: {self.basket.take_apple()}'


if __name__ == '__main__':
  Idunn().autodiscover('my_app')
  print(Feast(host='Bragi').serve())  # Bragi: youth restored

Run the example with python -m examples.my_app.main; there the discovery root is 'examples.my_app'.


Idunn

The three decorators

  • @Port

    Marks a typing.Protocol as a contract. Anything that is not a Protocol raises InvalidPortError. The Protocol is made runtime_checkable so Idunn can confirm an adapter satisfies it.

  • @Adapter(port, lifecycle=...)

    Declares a concrete class as the implementation of port. It attaches metadata and constructs nothing. The class must satisfy the Protocol, by inheritance (recommended) or structurally; Idunn never patches it to fit. One adapter per port is the normal case. lifecycle is TRANSIENT by default (a new instance per resolve) or SINGLETON (built once, reused).

    Selecting between several adapters with key= and envs= is covered in Advanced.

  • @Invert()

    Wraps a consumer's __init__. Every parameter whose type hint is a @Port is resolved when the constructor runs, assigned to self.<name>, and passed into the body. Three things to know:

    • A caller-supplied argument always wins, so Feast(basket=fake, host='x') skips injection and keeps the class easy to test.
    • Resolution is recursive: an adapter's own @Port constructor parameters are resolved first.
    • Constructing an @Invert() object is the only way to start a graph. There is no resolve(). Always apply it with parentheses. Keyed selection, explicit parameter maps, and optional dependencies are in Advanced.

Idunn

What gets registered

Only classes wearing the decorator, defined at module level in a module under the root. A class that satisfies a port but lacks @Adapter(...) is invisible to discovery. So is an undecorated subclass of an adapter, a decorated class in a test module or an excluded one, and a decorated class imported from another package (discover that package as its own root). Looking the part is not the same as wearing the badge.


Idunn

Development

poetry install --with dev
poetry run pytest
poetry run ruff format --check . && poetry run ruff check .
poetry run mypy

CI (.github/workflows/ci.yml) runs the same checks on Python 3.11, 3.12, and 3.13.16. Idunn supports Python 3.11 and newer (requires-python = ">=3.11,<4.0") and is developed on 3.13.16.


Idunn

Going further

  • About Idunn: the model and the reasons behind the short "no" list.
  • Advanced: environments and keys, the full @Invert contract, discovery options, describe(), test isolation, exceptions, limitations, and maintainer notes.
  • Class catalog: every class in the package, public or hidden.

Metadata

Release files for idunn 1.0.1

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

Source distribution (sdist)

Source distribution for idunn 1.0.1
File Size Uploaded Provenance Actions
idunn-1.0.1.tar.gz 21.7 kB Without provenance Details for idunn-1.0.1.tar.gz

Built distribution (wheel)

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

Total release size: 49.7 kB

Release files / idunn-1.0.1.tar.gz

Download URL idunn-1.0.1.tar.gz
Size 21.7 kB
Tags Source
SHA-256 checksum
How to use checksums
fae9613360ee94418346250adaafbab5a73ec14bedbb5d47be2a7cd70f5493c9
BLAKE2b-256 checksum
How to use checksums
ec51a038d43e9e2b9fc2c55d1ac972757a0b7004729b61bc2baf299ef22099e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.5.1 CPython/3.14.4 Darwin/27.0.0

Release files / idunn-1.0.1-py3-none-any.whl

Download URL idunn-1.0.1-py3-none-any.whl
Size 28.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f59fd399cc13f1e70039a32b02b4f548813ca231556cd9bf48b7b9ccd91f864e
BLAKE2b-256 checksum
How to use checksums
6f841b8aa15da9eedb0ef21c4dfc992f32debf362435d2ac141a02a79c5b8434
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.5.1 CPython/3.14.4 Darwin/27.0.0

Security for idunn 1.0.1

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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