Skip to content

Getting Started: 5-Minute Tutorial

Welcome to pico-ioc! 🚀

This guide will get you installed and running your first dependency-injected application in less than 5 minutes. We'll go from an empty file to a fully-wired application.

By the end of this tutorial, you will understand the three most fundamental APIs in pico-ioc:

  1. @component: The decorator that registers your classes with the container.
  2. init(): The function that scans your project and builds the container.
  3. container.get(): The method you use to ask the container for your final, fully-wired object.

1. Installation

pico-ioc requires Python 3.11 or newer.

You can install it directly from PyPI using pip:

pip install pico-ioc

This core package includes all the necessary logic. For advanced, tree-based configuration from YAML files (covered in the User Guide), you can install the optional dependency:

pip install pico-ioc[yaml]

2. The 5-Minute Tutorial

Let's build a simple "Hello, World!" application.

Step 1: Define Your Components

First, create a file named app.py. We'll define two classes:

  1. GreeterService: A simple service with one job: providing a greeting.
  2. App: Our main application class, which depends on the GreeterService.

The @component decorator is the core of pico-ioc. It's a marker that tells the container: "Find this class, scan its dependencies, and make it available for injection."

# app.py
from pico_ioc import component

@component
class GreeterService:
    """A service that provides a greeting."""
    def greet(self) -> str:
        return "Hello, pico-ioc!"

@component
class App:
    """Our main application class."""

    # pico-ioc will see this and inject GreeterService
    def __init__(self, greeter: GreeterService):
        self.greeter = greeter

    def run(self):
        # Use the injected service
        print(self.greeter.greet())

What's happening here?

  • @component on GreeterService tells pico-ioc to register this class. It has no dependencies.
  • @component on App also registers it.
  • def __init__(self, greeter: GreeterService): This is the magic. pico-ioc reads your constructor's type hints. It sees that App requires an instance of GreeterService and understands that it must provide one when building an App.

Step 2: Initialize the Container

Now, at the bottom of your app.py, we need to tell pico-ioc to find these components. We do this by calling init().

init() is the function that builds the container. Its most important argument is modules, which tells it where to look for @component decorators.

# app.py
from pico_ioc import component, init

# ... (GreeterService and App classes from above) ...

if __name__ == "__main__":
    # Scan the current module (this file)
    # and find all @component decorators
    container = init(modules=[__name__])

What's happening here?

  • if __name__ == "__main__": ensures this code only runs when the script is executed directly.
  • container = init(modules=[__name__]): This line does all the work:
  • It scans the module specified (in this case, __name__ refers to the current file, app.py).
  • It finds GreeterService and App.
  • It validates their dependencies (sees that App needs GreeterService and that GreeterService is available).
  • It returns a PicoContainer instance, which is now a fully-configured factory ready to build your components.

Step 3: Get Your Component and Run

The container object now knows how to build App and its entire dependency tree. We can ask for the final App instance using container.get() and then call its run() method.

# app.py
from pico_ioc import component, init

# ... (GreeterService and App classes from above) ...

if __name__ == "__main__":
    container = init(modules=[__name__])

    # Ask the container for the 'App' instance
    app = container.get(App)

    # Run the application
    app.run()

What's happening here?

When you call container.get(App), pico-ioc performs a "depth-first" resolution:

  1. It sees you want an App.
  2. It checks App's dependencies and sees it needs a GreeterService.
  3. It checks GreeterService's dependencies (it has none).
  4. It creates the GreeterService instance (and caches it for future use).
  5. It creates the App instance, injecting the GreeterService instance into its constructor.
  6. It returns the fully-wired App instance to you.

3. Putting It All Together

Here is the complete, runnable app.py file.

# app.py
from pico_ioc import component, init

# 1. Define components
@component
class GreeterService:
    """A service that provides a greeting."""
    def greet(self) -> str:
        return "Hello, pico-ioc!"

@component
class App:
    """Our main application class."""

    # 2. Define dependencies via type hints
    def __init__(self, greeter: GreeterService):
        self.greeter = greeter

    def run(self):
        # Use the injected service
        print(self.greeter.greet())

# 3. Initialize and run
if __name__ == "__main__":
    # Scans this module and finds App/GreeterService
    container = init(modules=[__name__])

    # Resolves App and its GreeterService dependency
    app = container.get(App)

    app.run()

Run it from your terminal:

$ python app.py
Hello, pico-ioc!

4. Next Steps

Congratulations! You've successfully built your first pico-ioc application.

You've learned the three core APIs: @component to register classes, init() to build the container, and container.get() to resolve components.

Now you're ready to explore the core features in the User Guide: - User Guide: ./user-guide/README.md


5. Tips and Troubleshooting

  • Make sure every class you want the container to build is annotated with @component.
  • Use type hints on __init__ parameters so pico-ioc can understand dependencies.
  • Ensure you pass the correct modules to init(modules=[...]) where your components are defined.
  • If you see resolution errors, check for:
  • Missing @component on a dependency class.
  • Typos in type hints.
  • Circular dependencies between components.