Skip to main content

Flexible structural typing and runtime validation for Python

Project description

๐Ÿฆ† Duckdantic

PyPI Python Version License Tests Documentation

๐Ÿš€ Structural typing and runtime validation for Python

If it walks like a duck and quacks like a duck, then it's probably a duck

๐Ÿ“š Documentation โ€ข ๐ŸŽฏ Examples โ€ข ๐Ÿ“ฆ PyPI


๐ŸŒŸ What is Duckdantic?

Duckdantic brings true structural typing to Python runtime! Check if objects satisfy interfaces without inheritance, validate data shapes dynamically, and build flexible APIs that work with any compatible object.

Perfect for microservices, plugin architectures, and polyglot systems where you need structural compatibility without tight coupling.

โœจ Key Features

Feature Description
๐Ÿฆ† True Duck Typing Runtime structural validation without inheritance
โšก High Performance Intelligent caching and optimized field normalization
๐Ÿ”Œ Universal Compatibility Works with Pydantic, dataclasses, TypedDict, attrs, and plain objects
๐ŸŽฏ Flexible Policies Customize validation behavior (strict, lenient, coercive)
๐Ÿ—๏ธ Protocol Integration Drop-in compatibility with Python's typing.Protocol
๐Ÿ” ABC Support Use with isinstance() and issubclass()
๐ŸŽจ Ergonomic API Clean, intuitive interface with excellent IDE support

๐Ÿ“ฆ Installation

# Basic installation
pip install duckdantic

# With Pydantic support (recommended)
pip install "duckdantic[pydantic]"

# Development installation
pip install "duckdantic[all]"

๐Ÿš€ Quick Start

The Duck API (Most Popular)

Perfect for Pydantic users and modern Python development:

from pydantic import BaseModel
from duckdantic import Duck

# Define your models
class User(BaseModel):
    name: str
    email: str
    age: int
    is_active: bool = True

class Person(BaseModel):
    name: str
    age: int

# Create a structural type
PersonShape = Duck(Person)

# โœ… Structural validation - no inheritance needed!
user = User(name="Alice", email="alice@example.com", age=30)
assert isinstance(user, PersonShape)  # True! User has required Person fields

# ๐Ÿ”„ Convert between compatible types
person = PersonShape.convert(user)  # Person(name="Alice", age=30)

Universal Compatibility

Works with any Python object:

from dataclasses import dataclass
from typing import TypedDict
from duckdantic import Duck

# Works with dataclasses
@dataclass
class DataPerson:
    name: str
    age: int

# Works with TypedDict
class DictPerson(TypedDict):
    name: str
    age: int

# Works with plain objects
class PlainPerson:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

# One duck type validates them all!
PersonShape = Duck.from_fields({"name": str, "age": int})

# โœ… All of these work
assert isinstance(DataPerson("Bob", 25), PersonShape)
assert isinstance(DictPerson(name="Charlie", age=35), PersonShape)  
assert isinstance(PlainPerson("Diana", 28), PersonShape)

Advanced: Trait-Based Validation

For complex structural requirements:

from duckdantic import TraitSpec, FieldSpec, satisfies

# Define a complex trait
APIResponse = TraitSpec(
    name="APIResponse",
    fields=(
        FieldSpec("status", int, required=True),
        FieldSpec("data", dict, required=True),
        FieldSpec("message", str, required=False),
        FieldSpec("timestamp", str, required=False),
    )
)

# Validate any object structure
response1 = {"status": 200, "data": {"users": []}}
response2 = {"status": 404, "data": {}, "message": "Not found"}

assert satisfies(response1, APIResponse)  # โœ… 
assert satisfies(response2, APIResponse)  # โœ…

Method Validation

Ensure objects implement required methods:

from duckdantic import MethodSpec, methods_satisfy

# Define method requirements (like typing.Protocol)
Drawable = [
    MethodSpec("draw", params=[int, int], returns=None),
    MethodSpec("get_bounds", params=[], returns=tuple),
]

class Circle:
    def draw(self, x: int, y: int) -> None:
        print(f"Drawing circle at ({x}, {y})")
    
    def get_bounds(self) -> tuple:
        return (0, 0, 10, 10)

assert methods_satisfy(Circle, Drawable)  # โœ…

๐ŸŽฏ Common Use Cases

๐ŸŒ Microservices & APIs

from duckdantic import Duck
from fastapi import FastAPI
from pydantic import BaseModel

class CreateUserRequest(BaseModel):
    name: str
    email: str

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    created_at: str

# Ensure response compatibility across services
ResponseShape = Duck(UserResponse)

app = FastAPI()

@app.post("/users/")
def create_user(request: CreateUserRequest):
    # Any object with the right shape works
    response = build_user_response(request)  # From any source
    assert isinstance(response, ResponseShape)  # Validation
    return response

๐Ÿ”Œ Plugin Systems

from duckdantic import Duck

# Define plugin interface
class PluginInterface:
    name: str
    version: str
    def execute(self, data: dict) -> dict: ...

PluginShape = Duck(PluginInterface)

# Load plugins from anywhere
def load_plugin(plugin_class):
    if isinstance(plugin_class(), PluginShape):
        return plugin_class()
    raise ValueError("Invalid plugin structure")

๐Ÿงช Testing & Mocking

from duckdantic import Duck

ProductionModel = Duck(YourProductionClass)

# Create test doubles that satisfy production interfaces
class MockService:
    def __init__(self):
        self.required_field = "test"
        self.another_field = 42

mock = MockService()
assert isinstance(mock, ProductionModel)  # โœ… Valid test double

๐Ÿ”— Related Projects

Project Relationship When to Use
Pydantic ๐Ÿค Perfect Companion Use Pydantic for data validation, Duckdantic for structural typing
typing.Protocol ๐Ÿ”„ Runtime Alternative Protocols are static, Duckdantic is runtime + dynamic
attrs โœ… Fully Compatible Define classes with attrs, validate with Duckdantic
dataclasses โœ… Fully Compatible Built-in support for dataclass validation
TypedDict โœ… Fully Compatible Validate dictionary structures dynamically

๐Ÿ—๏ธ Architecture Patterns

Hexagonal Architecture

# Define port interfaces with Duck types
UserRepositoryPort = Duck.from_methods({
    "save": (User,) -> User,
    "find_by_id": (int,) -> Optional[User],
})

# Any implementation that matches the structure works
class SQLUserRepository:
    def save(self, user: User) -> User: ...
    def find_by_id(self, user_id: int) -> Optional[User]: ...

assert isinstance(SQLUserRepository(), UserRepositoryPort)  # โœ…

CQRS Pattern

# Command/Query interfaces as Duck types
Command = Duck.from_fields({"command_id": str, "timestamp": datetime})
Query = Duck.from_fields({"query_id": str, "filters": dict})

# Any object with the right shape is valid
CreateUserCommand = {"command_id": "123", "timestamp": datetime.now(), "user_data": {...}}
assert isinstance(CreateUserCommand, Command)  # โœ…

๐Ÿ“Š Performance

Duckdantic is built for production performance:

  • Intelligent caching: Field analysis cached per type
  • Lazy evaluation: Only validates what's needed
  • Zero-copy operations: Minimal object creation overhead
  • Optimized for common patterns: Special handling for Pydantic/dataclasses
# Benchmark: 1M validations
import timeit
from duckdantic import Duck

PersonShape = Duck.from_fields({"name": str, "age": int})
test_obj = {"name": "test", "age": 25}

time_taken = timeit.timeit(
    lambda: isinstance(test_obj, PersonShape),
    number=1_000_000
)
print(f"1M validations: {time_taken:.2f}s")  # ~0.5s on modern hardware

๐Ÿ› ๏ธ Advanced Configuration

Custom Validation Policies

from duckdantic import Duck, ValidationPolicy

# Strict policy: exact type matching
strict_duck = Duck(MyModel, policy=ValidationPolicy.STRICT)

# Lenient policy: duck typing with coercion
lenient_duck = Duck(MyModel, policy=ValidationPolicy.LENIENT)

# Custom policy: your own rules
class CustomPolicy(ValidationPolicy):
    def validate_field(self, value, expected_type):
        # Your custom validation logic
        return custom_validation(value, expected_type)

custom_duck = Duck(MyModel, policy=CustomPolicy())

Integration with Type Checkers

from typing import TYPE_CHECKING
from duckdantic import Duck

if TYPE_CHECKING:
    # Static type checking with Protocol
    from typing import Protocol
    
    class PersonProtocol(Protocol):
        name: str
        age: int
else:
    # Runtime validation with Duck
    PersonProtocol = Duck.from_fields({"name": str, "age": int})

# Works with both mypy and runtime!
def process_person(person: PersonProtocol) -> str:
    return f"{person.name} is {person.age} years old"

๐Ÿ“š Documentation

Resource Description
๐Ÿ“– Getting Started Complete setup and basic usage guide
๐ŸŽฏ Examples Real-world usage patterns and recipes
๐Ÿ”ง API Reference Complete API documentation
๐Ÿ—๏ธ Architecture Guide Design patterns and best practices
โšก Performance Guide Optimization tips and benchmarks

๐Ÿค Contributing

We love contributions! Duckdantic is built by the community, for the community.

# Quick setup
git clone https://github.com/pr1m8/duckdantic.git
cd duckdantic
pip install -e ".[dev]"
pytest  # Run tests

See our Contributing Guide for detailed instructions.

๐ŸŽ‰ Community

๐Ÿ“„ License

Licensed under the MIT License - see LICENSE for details.

๐Ÿ™ Acknowledgments

  • TypeScript and Go interfaces for structural typing inspiration
  • Pydantic for showing how beautiful Python validation can be
  • Protocol for static structural typing patterns
  • The Python community for embracing duck typing as a core principle

"In the face of ambiguity, refuse the temptation to guess."
โ€” The Zen of Python

Duckdantic: Where structure meets flexibility ๐Ÿฆ†โœจ

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

duckdantic-1.0.3.tar.gz (497.6 kB view details)

Uploaded Source

Built Distribution

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

duckdantic-1.0.3-py3-none-any.whl (34.4 kB view details)

Uploaded Python 3

File details

Details for the file duckdantic-1.0.3.tar.gz.

File metadata

  • Download URL: duckdantic-1.0.3.tar.gz
  • Upload date:
  • Size: 497.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.8

File hashes

Hashes for duckdantic-1.0.3.tar.gz
Algorithm Hash digest
SHA256 6f060cbc349846cee89ea36805b0fad944025fa3812f3c7ffa5ce6f13e9ccdbe
MD5 b3da272523e417b4ec573b31c3f32f55
BLAKE2b-256 805bc7b43ca767cacb0622e67e49810699076e0477399b02aa838392811fd3f7

See more details on using hashes here.

File details

Details for the file duckdantic-1.0.3-py3-none-any.whl.

File metadata

  • Download URL: duckdantic-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 34.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.8

File hashes

Hashes for duckdantic-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 cd102190ee55e8489bf365734b757e0ce59036165b9e6ef03e93a804ae50251f
MD5 f37797dd5b6ac2c67c081e031798a9ad
BLAKE2b-256 80e0215cda4a5aba4aa614acb6d2225a8077d68977b9ff1520803898ea0a09cc

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