DEV Community

Kuyugáma Hikámiya
Kuyugáma Hikámiya

Posted on

FunDI — Lightweight Dependency Injection for Functional Python

Dependency Injection in Python is usually tied to frameworks.

If you use FastAPI, you get DI inside request scope.
If you use aiogram, you get implicit injection with flags and hidden magic.

But what if you want:

  • Explicit dependency wiring
  • Functional style
  • Framework-independent injection
  • Clean testability

That’s why I built FunDI.

FunDI — lightweight dependency injection library for functional programming.
It helps inject dependencies in a simple and declarative way.


Why another DI library?

Most framework DIs are:

  • Implicit
  • Request-bound
  • Harder to debug at scale

For example, in aiogram:

@router.message(F.text == "/whoami", flags={"requires": {User: "user"}})
async def on_whoami(message: Message, user: User):
    ...
Enter fullscreen mode Exit fullscreen mode

You can’t easily see:

  • where user is created
  • how it’s resolved
  • whether it’s even configured correctly at startup

FunDI makes dependency origin explicit:

from fundi import from_

from bo.dependencies import require_user

async def on_whoami(user: User = from_(require_user)):
    ...
Enter fullscreen mode Exit fullscreen mode

No flags.
No hidden injection.
No guessing.

You see exactly where the value comes from.


Core Idea

In FunDI, everything revolves around functions.

Key Concepts

  • Dependency — function that produces data
  • Dependant — function that consumes dependencies
  • Scope — injection startup environment
  • Injection — resolving arguments from scope + dependency graph
  • Lifespan dependency — setup + teardown (generator with single yield)
  • Side effects — dependencies whose products are not passed into dependants
  • Hooks — injection lifecycle events
  • Overriding — swap dependencies for testing

It’s intentionally minimal and composable.


Quick Start

from fundi import scan, from_, inject


def require_user():
    return "Alice"


def greet(user: str = from_(require_user)):
    print(f"Hello, {user}!")


inject({}, scan(greet))
Enter fullscreen mode Exit fullscreen mode

That’s it.

No app object.
No request.
No container class.

Just functions.


Features

  • Simple syntax — define dependency with from_()
  • Flexible dependency resolving algorithm
  • Dependency overriding
  • Built-in dependency mocking
  • Works outside any framework

Caching Behavior

By default, dependencies are called once per injection.

This prevents unnecessary repetition and guarantees consistent resolution.

You can disable caching:

from_(require_something, caching=False)
Enter fullscreen mode Exit fullscreen mode

or

scan(func, caching=False)
Enter fullscreen mode Exit fullscreen mode

Note: disabling caching applies only to the specified function, not its internal dependencies.


Lifespan Dependencies

FunDI supports two-phase dependencies using generator functions:

def acquire_resource():
    resource = connect()
    yield resource
    resource.close()
Enter fullscreen mode Exit fullscreen mode

Preparation before yield.
Cleanup after.

You can also use context manager classes:

  • __enter__ / __exit__
  • __aenter__ / __aexit__

This makes resource handling explicit and deterministic.


Async Support

Async dependencies work exactly the same:

async def require_random_name() -> str:
    await asyncio.sleep(0.4)
    return random.choice(("Bob", "Steve", "Petro"))
Enter fullscreen mode Exit fullscreen mode

FunDI resolves sync and async dependencies seamlessly.


Naming Convention

Dependency names communicate behavior:

  • require_ → may raise
  • optional_ → may return None
  • acquire_ → resource lifecycle dependency

This keeps business logic clean:

async def handler(admin: User = from_(require_admin_user)):
    ...
Enter fullscreen mode Exit fullscreen mode

Clear separation between parameter name and provider function.


Comparison

Feature FunDI Aiogram FastAPI
Implicit DI Optional Yes Partial
Framework-bound No Yes Yes

FunDI keeps what works from FastAPI’s design, but removes request coupling.


Testing & Overriding

One of the most important features.

Dependencies can be overridden during injection.
This makes testing trivial:

  • no patching globals
  • no monkeypatch gymnastics
  • no container rewiring

You control resolution at injection level.


Philosophy

FunDI is intentionally small.

It does one thing:
resolve dependencies in a declarative, explicit, composable way.

It does not:

  • manage application lifecycle
  • replace frameworks
  • impose architectural patterns

It simply wires functions together cleanly.


Installation

pip install fundi
Enter fullscreen mode Exit fullscreen mode

or

poetry add fundi
Enter fullscreen mode Exit fullscreen mode

or

uv add fundi
Enter fullscreen mode Exit fullscreen mode

When Should You Use It?

  • Building CLI tools
  • Writing pure services
  • Designing layered architectures
  • Testing business logic independently
  • Want FastAPI-like DI without HTTP coupling
  • Tired of implicit framework magic

FunDI is small, explicit, and predictable.

If you prefer seeing your dependency graph instead of guessing it — you might like it.

Documentation: fundi.readthedocs.org
Repository: github.com/KuyuCode/fundi
PyPI: pypi.org/project

Top comments (0)