Flexible structural typing and runtime validation for Python
Project description
๐ฆ Duckdantic
๐ 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
- ๐ Found a bug? Open an issue
- ๐ก Have an idea? Start a discussion
- ๐ Need help? Check our documentation
- ๐ Like the project? Give us a star on GitHub!
๐ 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file duckdantic-1.0.6.tar.gz.
File metadata
- Download URL: duckdantic-1.0.6.tar.gz
- Upload date:
- Size: 501.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63be551b68d6ba31afafd8f28b814d4390586e14a4e3db19a5e67a4b17279123
|
|
| MD5 |
1adf8e61bb3c78e5eba147e68787be85
|
|
| BLAKE2b-256 |
5c8751888f02d5b64a087708f93eb409b3b3c0875cd80dabdd3a72ab5bf2bee4
|
File details
Details for the file duckdantic-1.0.6-py3-none-any.whl.
File metadata
- Download URL: duckdantic-1.0.6-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
002e7fb4cd03f5bd4618bce0726bea64b38a37edbbf3cab94615f61630bda976
|
|
| MD5 |
a129caae6d58e78bb88f9a9dc947cfd1
|
|
| BLAKE2b-256 |
5e62aa0dedb647c84d26a4ef7df721c5e7df6feb646472f7385f0447bbeae4ae
|