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

๐Ÿ”ฌ Type Algebra & Relationships

Duckdantic implements a complete algebraic type system with mathematical properties that enable powerful type composition and analysis:

Set-Theoretic Operations

from duckdantic import Duck, TraitSpec, FieldSpec

# Define base types
Person = Duck.from_fields({"name": str, "age": int})
Employee = Duck.from_fields({"name": str, "age": int, "employee_id": str})
Manager = Duck.from_fields({"name": str, "age": int, "employee_id": str, "team": list})

# Subtyping relationships (Employee โІ Person)
assert Employee <= Person  # Employee is a subtype of Person
assert Manager <= Employee  # Manager is a subtype of Employee
assert Manager <= Person   # Transitivity holds!

# Type intersection (A โˆฉ B)
HasName = Duck.from_fields({"name": str})
HasAge = Duck.from_fields({"age": int})
PersonIntersect = HasName & HasAge  # Creates intersection type

# Type union (A โˆช B)
EmailUser = Duck.from_fields({"email": str})
PhoneUser = Duck.from_fields({"phone": str})
ContactableUser = EmailUser | PhoneUser  # Either email OR phone

# Type algebra satisfies mathematical laws
assert (A <= B and B <= C) implies (A <= C)  # Transitivity
assert (A & B) <= A and (A & B) <= B         # Intersection property
assert A <= (A | B) and B <= (A | B)         # Union property

Structural Width & Depth Subtyping

# Width subtyping: More fields = more specific
Basic = Duck.from_fields({"id": int})
Extended = Duck.from_fields({"id": int, "name": str})
assert Extended <= Basic  # Extended is a subtype (has more fields)

# Depth subtyping: More specific field types
Generic = Duck.from_fields({"items": list})
Specific = Duck.from_fields({"items": list[str]})
assert Specific <= Generic  # Specific is a subtype (more precise type)

# Combined width + depth
BaseAPI = Duck.from_fields({"status": int})
DetailedAPI = Duck.from_fields({"status": int, "data": dict[str, Any], "meta": dict})
assert DetailedAPI <= BaseAPI  # Satisfies both width and depth

Algebraic Properties

Duckdantic's type system satisfies important algebraic properties:

Property Definition Example
Reflexivity A โІ A Person <= Person is always true
Antisymmetry A โІ B โˆง B โІ A โŸน A = B If types satisfy each other, they're equivalent
Transitivity A โІ B โˆง B โІ C โŸน A โІ C Subtyping chains work as expected
Join Existence โˆ€A,B โˆƒ(A โˆจ B) Union types always exist
Meet Existence โˆ€A,B โˆƒ(A โˆง B) Intersection types always exist

Practical Applications

# Type-safe function composition
def process_person(obj: Person) -> dict:
    """Accepts any object satisfying Person shape"""
    return {"name": obj.name.upper(), "adult": obj.age >= 18}

def process_employee(obj: Employee) -> dict:
    """More specific - requires employee_id too"""
    result = process_person(obj)  # Safe! Employee <= Person
    result["emp_id"] = obj.employee_id
    return result

# Automatic type narrowing
manager = Manager(name="Alice", age=35, employee_id="M001", team=["Bob", "Charlie"])
assert isinstance(manager, Person)    # โœ… True
assert isinstance(manager, Employee)  # โœ… True
assert isinstance(manager, Manager)   # โœ… True

# Use in generic contexts
from typing import TypeVar

T = TypeVar('T', bound=Person)

def birthday(person: T) -> T:
    """Works with any Person-like type"""
    person.age += 1
    return person

# Works with all subtypes!
birthday(manager)  # Manager in, Manager out

Type Lattice Visualization

        โŠค (Top - Empty type)
         |
      Manager
         |
      Employee
         |
       Person
         |
    HasName โˆฉ HasAge
       /   \
   HasName  HasAge
       \   /
        โŠฅ (Bottom - Any type)

This algebraic foundation enables:

  • Type inference: Automatically determine most general valid types
  • Type checking: Mathematically prove type safety
  • Type optimization: Find minimal type representations
  • Composition: Build complex types from simple ones

๐Ÿ—๏ธ 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)  # โœ…

๐Ÿ”— Type Relationships & Operators

Duckdantic provides rich comparison operators for type analysis:

Comparison Operators

from duckdantic import Duck

# Define a type hierarchy
Animal = Duck.from_fields({"name": str})
Dog = Duck.from_fields({"name": str, "breed": str})
Poodle = Duck.from_fields({"name": str, "breed": str, "fluffy": bool})

# Subtype checking (<=, >=)
assert Poodle <= Dog <= Animal  # Poodle is most specific
assert Animal >= Dog >= Poodle  # Animal is most general

# Strict subtype (<, >)
assert Poodle < Dog < Animal   # Strictly more specific
assert Animal > Dog > Poodle   # Strictly more general

# Type equality (==)
Dog2 = Duck.from_fields({"name": str, "breed": str})
assert Dog == Dog2  # Same structure = equal types

# Type compatibility (~)
class MyDog:
    name: str = "Fido"
    breed: str = "Labrador"

assert MyDog() ~ Dog  # MyDog instance satisfies Dog shape

Advanced Type Operations

# Type difference
RequiredFields = Employee - Person  # Fields in Employee but not Person
# Result: {"employee_id": str}

# Type complement
NotPerson = ~Person  # Types that don't satisfy Person

# Conditional types
Adult = Person.where(lambda p: p.age >= 18)
Senior = Person.where(lambda p: p.age >= 65)

# Type guards
def process_contact(contact: EmailUser | PhoneUser):
    if contact ~ EmailUser:
        send_email(contact.email)
    elif contact ~ PhoneUser:
        send_sms(contact.phone)

๐Ÿ“Š 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.4.tar.gz (501.3 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.4-py3-none-any.whl (36.2 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for duckdantic-1.0.4.tar.gz
Algorithm Hash digest
SHA256 c87205f5f96745e0afafcb7ff7f6eb1ae53316894d442431d0bcb8754d5dff4a
MD5 98870876f77f39817eaab229ac72b875
BLAKE2b-256 1705eaa8e8438b8382d24cd572038bdcadf1a163fdeebc24e6f3bee71e59ca42

See more details on using hashes here.

File details

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

File metadata

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

File hashes

Hashes for duckdantic-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 44991ce7dac1a4985b88d6bf319f3505bc7a3577869d670e513fec10c05dab5d
MD5 97d0fe746471d175c0d1ccc4bd621efd
BLAKE2b-256 0ebea5c9e19d00f9671365b3ca5595d30f68505759bc6d6ff7cea64bd6a32e64

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