Skip to main content

Production-grade service locator and dependency injection container for Python with thread-safe singletons, lazy loading, and factory patterns

Project description

GetIt: Production-Grade Service Locator & Dependency Injection Container

Python Version License: MIT Code Style: Black

GetIt is a thread-safe, production-ready service locator and dependency injection container for Python. It provides elegant, decoupled architecture through declarative registration, multiple resolution strategies, and automatic lifecycle management—all without the boilerplate.

Designed for WSGI/ASGI applications, microservices, and complex Python projects that need flexible, maintainable dependency management.

Features

  • Thread-Safe: Double-checked locking guarantees singletons instantiate exactly once under concurrent load
  • Multiple Lifecycles: Singleton, Lazy Singleton, and Factory patterns for flexible dependency management
  • Zero-Import Resolution: Fetch dependencies at runtime without circular imports
  • Hybrid Resolution APIs: Class-based, dot-notation, and explicit getter syntaxes
  • Declarative Registration: Use @Singleton, @LazySingleton, @Factory decorators at point of definition
  • Full Type Hints: Complete type annotations with @overload for IDE autocomplete in VS Code and PyCharm
  • Production Logging: Structured logging for observability and debugging
  • Comprehensive Error Handling: Custom exceptions with actionable error messages

Quick Start

Installation

pip install getit-python

Basic Example

from get_it import GetIt, singleton

@singleton
class Service:
    def sync(self):
        print("Syncing.....")

# Global resolution
service = GetIt.get(Service)
service.sync()  # Output: Syncing.....

Complete Usage Guide

1. Defining Services with Decorators

Services are registered where they're defined, keeping code organized and discoverable.

Singleton (Eager Initialization)

Instantiated immediately at decorator evaluation time. Use for lightweight config or static resources.

from get_it import GetIt, singleton

@singleton
class AppConfig:
    def __init__(self):
        self.debug = True
        self.host = "localhost"
        self.port = 8000
        print("[CONFIG] Initialized at import time")

# Automatically registered and instantiated
config = GetIt.get(AppConfig)
print(config.host)  # "localhost"

Lazy Singleton (Deferred Initialization)

Instantiated only on first access. Perfect for expensive operations like database connections.

from get_it import GetIt, LazySingleton

@LazySingleton
class DatabaseService:
    def __init__(self):
        print("[DB] Connecting to database...")
        self.connected = True

    def query(self, sql: str):
        return f"Executing: {sql}"

# Not instantiated yet
print("App starting...")

# First access triggers initialization
db = GetIt.get(DatabaseService)  # Prints: [DB] Connecting to database...
result = db.query("SELECT * FROM users")

Factory (Fresh Instance Per Request)

Creates a new instance every time. Ideal for request-scoped state or temporary objects.

from get_it import GetIt, Factory

@Factory
class RequestContext:
    def __init__(self):
        self.request_id = id(self)
        self.user_data = {}

# Each call gets a fresh instance
ctx1 = GetIt.get(RequestContext)
ctx2 = GetIt.get(RequestContext)

assert ctx1 is not ctx2  # Different instances
assert ctx1.request_id != ctx2.request_id

2. Resolution Strategies

GetIt supports multiple ways to resolve dependencies. Choose the pattern that best fits your codebase.

Strategy A: Dot-Notation (Cleanest)

Flutter-style syntax. Resolves by class name matching.

from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class DatabaseService:
    def connect(self) -> None:
        print("--> Establishing Database Connection...")

@LazySingleton
class UserRepository:
    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Alice"}

# Dot-notation resolution
db: DatabaseService = get_it.DatabaseService
db.connect()

repo: UserRepository = get_it.UserRepository
user = repo.get_user(1)
print(user)

Strategy B: Instance Call

Pass the type directly to the locator instance.

from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class CacheService:
    def set(self, key: str, value: str):
        print(f"Cache: {key} = {value}")

# Instance call syntax
cache = get_it(CacheService)
cache.set("user_123", "cached_data")

Strategy C: Metaclass Resolution

Call the class directly for explicit type-based resolution.

from get_it import GetIt, LazySingleton

@LazySingleton
class AuthService:
    def verify_token(self, token: str) -> bool:
        return len(token) > 10

# Metaclass resolution
auth = GetIt(AuthService)
is_valid = auth.verify_token("my_token_12345")
print(f"Valid: {is_valid}")

Strategy D: Explicit Get

Standard, unambiguous method for any situation.

from get_it import GetIt, LazySingleton

@LazySingleton
class MailService:
    def send(self, to: str, message: str):
        print(f"Email to {to}: {message}")

# Explicit get
mail = GetIt.get(MailService)
mail.send("user@example.com", "Hello!")

Complete Example with All Strategies

from get_it import GetIt, LazySingleton

get_it = GetIt()

@LazySingleton
class DatabaseService:
    def connect(self) -> None:
        print("--> Establishing Database Connection...")

def handle_user_login():
    # Strategy A: Dot-notation (cleanest)
    db_a: DatabaseService = get_it.DatabaseService
    db_a.connect()

    # Strategy B: Instance call
    db_b = get_it(DatabaseService)
    db_b.connect()

    # Strategy C: Metaclass call
    db_c = GetIt(DatabaseService)
    db_c.connect()

    # Strategy D: Explicit getter
    db_d = GetIt.get(DatabaseService)
    db_d.connect()
    
    # All resolve to the same cached instance
    assert db_a is db_b is db_c is db_d

handle_user_login()

3. Real-World Web Framework Integration

Flask Example

from flask import Flask, jsonify
from get_it import GetIt, LazySingleton

app = Flask(__name__)
get_it = GetIt()

@LazySingleton
class UserDatabase:
    def __init__(self):
        print("[INIT] Connecting to PostgreSQL...")
        # self.connection = psycopg2.connect(...)

    def get_user(self, user_id: int):
        return {"id": user_id, "name": "Alice", "email": "alice@example.com"}

@LazySingleton
class AuthService:
    def __init__(self):
        self.db = get_it.UserDatabase

    def authenticate(self, user_id: int):
        user = self.db.get_user(user_id)
        return bool(user)

@app.route("/users/<int:user_id>")
def get_user(user_id: int):
    auth = GetIt.get(AuthService)
    if auth.authenticate(user_id):
        user = auth.db.get_user(user_id)
        return jsonify(user)
    return jsonify({"error": "Unauthorized"}), 401

if __name__ == "__main__":
    app.run(debug=True)

FastAPI Example

from fastapi import FastAPI, HTTPException
from get_it import GetIt, LazySingleton

app = FastAPI()
get_it = GetIt()

@LazySingleton
class UserService:
    async def get_user(self, user_id: int):
        # Simulated database call
        return {"id": user_id, "name": "Bob", "email": "bob@example.com"}

@app.get("/users/{user_id}")
async def read_user(user_id: int):
    service = GetIt.get(UserService)
    user = await service.get_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

4. Manual Registration

For runtime registration or testing scenarios:

from get_it import GetIt

class Logger:
    def log(self, msg: str):
        print(f"[LOG] {msg}")

# Manual registration
logger = Logger()
GetIt.register_singleton(logger)

# Resolution
retrieved_logger = GetIt.get(Logger)
retrieved_logger.log("Hello from manual registration!")

# Factory registration
GetIt.register_factory(Logger, lambda: Logger())

# Lazy singleton registration
GetIt.register_lazy_singleton(Logger, lambda: Logger())

5. Testing with GetIt

Isolate tests by resetting the container and registering mocks:

import pytest
from get_it import GetIt

class UserRepository:
    def get_user(self, user_id: int):
        # Real implementation
        return {"id": user_id, "name": "Real DB"}

class UserService:
    def __init__(self):
        self.repo = GetIt.get(UserRepository)

    def fetch_user(self, user_id: int):
        return self.repo.get_user(user_id)

@pytest.fixture
def clean_di():
    """Clean GetIt before and after each test."""
    GetIt.reset()
    yield
    GetIt.reset()

@pytest.fixture
def mock_user_repo(clean_di):
    """Provide a mock repository for testing."""
    class MockRepository:
        def get_user(self, user_id: int):
            return {"id": user_id, "name": "Mock User", "test": True}

    mock = MockRepository()
    GetIt.register_singleton(mock)
    return mock

def test_user_service_with_mock(mock_user_repo):
    service = UserService()
    user = service.fetch_user(1)
    
    assert user["name"] == "Mock User"
    assert user["test"] is True

Lifecycle Reference

Lifecycle When Initialized When Destroyed Best For
Singleton At decoration/registration time Application lifetime Static config, caches, shared state
LazySingleton On first access Application lifetime Database connections, API clients, services
Factory On every access Immediately after use Request-scoped state, temporary objects

Exception Handling

GetIt provides specific exceptions for precise error handling:

from get_it import (
    GetIt,
    DependencyNotRegisteredError,
    InvalidDependencyTypeError,
    LazySingleton
)

@LazySingleton
class Service:
    pass

try:
    # This will raise DependencyNotRegisteredError
    GetIt.get(str)
except DependencyNotRegisteredError as e:
    print(f"Not registered: {e}")

try:
    # This will raise InvalidDependencyTypeError
    GetIt.register_lazy_singleton("not_a_type", lambda: None)
except InvalidDependencyTypeError as e:
    print(f"Invalid type: {e}")

Performance Characteristics

Operation Complexity Notes
GetIt.get(Type) singleton O(1) Direct dict lookup, no lock
GetIt.get(Type) lazy (first) O(1) + factory Uses double-checked locking
GetIt.get(Type) lazy (cached) O(1) Direct dict lookup, no lock
GetIt.get(Type) factory O(1) + factory Creates new instance
get_it.ServiceName O(n) Linear search by name, n=registered types

Tip: For performance-critical paths, prefer explicit type-based resolution (GetIt.get(Type)) over dot-notation to avoid the O(n) name lookup.


Best Practices

Do

  • Register services at their definition point using decorators
  • Use LazySingleton for expensive operations (database, HTTP clients)
  • Use Factory for request-scoped or stateful objects
  • Type-hint all resolved variables for IDE autocomplete
  • Call GetIt.reset() in test teardown to prevent test pollution
  • Use logging to debug dependency resolution (enable DEBUG logs)

Don't

  • Create circular dependencies (register A that depends on B that depends on A)
  • Block on expensive I/O in @singleton constructors (use @LazySingleton instead)
  • Resolve dependencies in module-level code (defer to runtime)
  • Mix service registration across multiple files without clear organization
  • Register services inside request handlers (register during app startup)

Comparison with Alternatives

Feature GetIt dependency-injector injector manual DI
Thread Safety Yes (RLock) Yes Partial N/A
Lazy Singletons Yes Yes Yes Manual
Factory Pattern Yes Yes Yes Manual
Dot-Notation Yes No No N/A
Type Hints Full Partial Partial Full
Decorator Support Yes Yes Yes No
Learning Curve Low Medium Low High
Circular Import Safe Yes Yes Yes No

Contributing

Contributions are welcome! Please ensure:

  • All tests pass: python3 -B test_get_it.py
  • Code is type-checked: mypy get_it.py
  • Follow PEP 8 style guidelines

License

MIT License - see LICENSE file for details


Changelog

v1.0.0 (2026-06-18)

  • Initial release
  • Thread-safe singleton, lazy singleton, and factory patterns
  • Comprehensive type hints and error handling
  • Full documentation and test coverage

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

getit_python-1.0.0.tar.gz (25.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

getit_python-1.0.0-py3-none-any.whl (15.0 kB view details)

Uploaded Python 3

File details

Details for the file getit_python-1.0.0.tar.gz.

File metadata

  • Download URL: getit_python-1.0.0.tar.gz
  • Upload date:
  • Size: 25.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for getit_python-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b1c12040a3426fdaffa92dc55ef173b9c7ef744f923274d1217f51263688caf9
MD5 0e353df6f504861bc1a165142949e507
BLAKE2b-256 aa16984a994d5ead806d9888b3f1ac279a706e8334fed5d0daeeec8b8a438670

See more details on using hashes here.

File details

Details for the file getit_python-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: getit_python-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 15.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for getit_python-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ac49e54cdce6a8364f0e06a5b1afaef7520147236f4f5cf6c5e68dae25bcf9c1
MD5 04117edeb49a84997b0f5e1cac828f96
BLAKE2b-256 765a98ba87a668cbbb3a1f947fa4910746603e264434141b4918adb1cb1fdad1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page