Skip to content

pr1m8/duckdantic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

36 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ¦† Duckdantic

PyPI Downloads 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 β€’ πŸ™ GitHub


🌟 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 πŸ¦†βœ¨

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages