duckdantic


Nameduckdantic JSON
Version 1.1.1 PyPI version JSON
download
home_pageNone
SummaryFlexible structural typing and runtime validation for Python
upload_time2025-08-22 21:40:18
maintainerNone
docs_urlNone
authorNone
requires_python>=3.12
licenseNone
keywords duck-typing pydantic runtime-types structural-typing validation
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ๐Ÿฆ† Duckdantic

<div align="center">

[![PyPI](https://img.shields.io/pypi/v/duckdantic.svg)](https://pypi.org/project/duckdantic/)
[![Downloads](https://img.shields.io/pypi/dm/duckdantic.svg)](https://pypi.org/project/duckdantic/)
[![Python Version](https://img.shields.io/pypi/pyversions/duckdantic.svg)](https://pypi.org/project/duckdantic/)
[![License](https://img.shields.io/pypi/l/duckdantic.svg)](https://github.com/pr1m8/duckdantic/blob/main/LICENSE)
[![Tests](https://github.com/pr1m8/duckdantic/workflows/Test%20Documentation%20Build/badge.svg)](https://github.com/pr1m8/duckdantic/actions)
[![Documentation](https://readthedocs.org/projects/duckdantic/badge/?version=latest)](https://duckdantic.readthedocs.io/en/latest/?badge=latest)

**๐Ÿš€ 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](https://duckdantic.readthedocs.io/) โ€ข [๐ŸŽฏ Examples](https://github.com/pr1m8/duckdantic/tree/main/docs/examples) โ€ข [๐Ÿ“ฆ PyPI](https://pypi.org/project/duckdantic/) โ€ข [๐Ÿ™ GitHub](https://github.com/pr1m8/duckdantic)

</div>

---

## ๐ŸŒŸ 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](https://pydantic.dev/), [dataclasses](https://docs.python.org/3/library/dataclasses.html), [TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict), [attrs](https://www.attrs.org/), and plain objects |
| ๐ŸŽฏ **Flexible Policies** | Customize validation behavior (strict, lenient, coercive) |
| ๐Ÿ—๏ธ **Protocol Integration** | Drop-in compatibility with Python's [typing.Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol) |
| ๐Ÿ” **ABC Support** | Use with `isinstance()` and `issubclass()` |
| ๐ŸŽจ **Ergonomic API** | Clean, intuitive interface with excellent IDE support |

## ๐Ÿ“ฆ Installation

```bash
# 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:

```python
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:

```python
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:

```python
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:

```python
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

```python
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

```python
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

```python
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**](https://pydantic.dev/) | ๐Ÿค **Perfect Companion** | Use Pydantic for data validation, Duckdantic for structural typing |
| [**typing.Protocol**](https://docs.python.org/3/library/typing.html#typing.Protocol) | ๐Ÿ”„ **Runtime Alternative** | Protocols are static, Duckdantic is runtime + dynamic |
| [**attrs**](https://www.attrs.org/) | โœ… **Fully Compatible** | Define classes with attrs, validate with Duckdantic |
| [**dataclasses**](https://docs.python.org/3/library/dataclasses.html) | โœ… **Fully Compatible** | Built-in support for dataclass validation |
| [**TypedDict**](https://docs.python.org/3/library/typing.html#typing.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

```python
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

```python
# 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

```python
# 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
```python
# 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
```python
# 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

```python
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

```python
# 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

```python
# 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

```python
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

```python
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**](https://duckdantic.readthedocs.io/en/latest/getting-started/) | Complete setup and basic usage guide |
| [๐ŸŽฏ **Examples**](https://duckdantic.readthedocs.io/en/latest/examples/) | Real-world usage patterns and recipes |
| [๐Ÿ”ง **API Reference**](https://duckdantic.readthedocs.io/en/latest/api/) | Complete API documentation |
| [๐Ÿ—๏ธ **Architecture Guide**](https://duckdantic.readthedocs.io/en/latest/guide/architecture/) | Design patterns and best practices |
| [โšก **Performance Guide**](https://duckdantic.readthedocs.io/en/latest/guide/performance/) | Optimization tips and benchmarks |

## ๐Ÿค Contributing

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

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

See our [**Contributing Guide**](CONTRIBUTING.md) for detailed instructions.

## ๐ŸŽ‰ Community

- ๐Ÿ› **Found a bug?** [Open an issue](https://github.com/pr1m8/duckdantic/issues)
- ๐Ÿ’ก **Have an idea?** [Start a discussion](https://github.com/pr1m8/duckdantic/discussions)
- ๐Ÿ“– **Need help?** Check our [documentation](https://duckdantic.readthedocs.io/)
- ๐ŸŒŸ **Like the project?** Give us a star on [GitHub](https://github.com/pr1m8/duckdantic)!
- ๐Ÿ“ฆ **Install it!** Get it from [PyPI](https://pypi.org/project/duckdantic/)

## ๐Ÿ“„ License

Licensed under the **MIT License** - see [LICENSE](LICENSE.txt) for details.

## ๐Ÿ™ Acknowledgments

- **TypeScript** and **Go interfaces** for structural typing inspiration
- **[Pydantic](https://pydantic.dev/)** for showing how beautiful Python validation can be
- **[Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol)** for static structural typing patterns
- **The Python community** for embracing duck typing as a core principle

---

<div align="center">

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

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

</div>
            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "duckdantic",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.12",
    "maintainer_email": null,
    "keywords": "duck-typing, pydantic, runtime-types, structural-typing, validation",
    "author": null,
    "author_email": "duckdantic <william.astley@algebraicwealth.com>",
    "download_url": "https://files.pythonhosted.org/packages/06/36/33fbb60a525d254b54e7e4ca37bc77fd0de649fc65cee7ef556c415c0d65/duckdantic-1.1.1.tar.gz",
    "platform": null,
    "description": "# \ud83e\udd86 Duckdantic\n\n<div align=\"center\">\n\n[![PyPI](https://img.shields.io/pypi/v/duckdantic.svg)](https://pypi.org/project/duckdantic/)\n[![Downloads](https://img.shields.io/pypi/dm/duckdantic.svg)](https://pypi.org/project/duckdantic/)\n[![Python Version](https://img.shields.io/pypi/pyversions/duckdantic.svg)](https://pypi.org/project/duckdantic/)\n[![License](https://img.shields.io/pypi/l/duckdantic.svg)](https://github.com/pr1m8/duckdantic/blob/main/LICENSE)\n[![Tests](https://github.com/pr1m8/duckdantic/workflows/Test%20Documentation%20Build/badge.svg)](https://github.com/pr1m8/duckdantic/actions)\n[![Documentation](https://readthedocs.org/projects/duckdantic/badge/?version=latest)](https://duckdantic.readthedocs.io/en/latest/?badge=latest)\n\n**\ud83d\ude80 Structural typing and runtime validation for Python**\n\n*If it walks like a duck and quacks like a duck, then it's probably a duck*\n\n[\ud83d\udcda Documentation](https://duckdantic.readthedocs.io/) \u2022 [\ud83c\udfaf Examples](https://github.com/pr1m8/duckdantic/tree/main/docs/examples) \u2022 [\ud83d\udce6 PyPI](https://pypi.org/project/duckdantic/) \u2022 [\ud83d\udc19 GitHub](https://github.com/pr1m8/duckdantic)\n\n</div>\n\n---\n\n## \ud83c\udf1f What is Duckdantic?\n\n**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.\n\nPerfect for **microservices**, **plugin architectures**, and **polyglot systems** where you need structural compatibility without tight coupling.\n\n## \u2728 Key Features\n\n| Feature | Description |\n|---------|-------------|\n| \ud83e\udd86 **True Duck Typing** | Runtime structural validation without inheritance |\n| \u26a1 **High Performance** | Intelligent caching and optimized field normalization |  \n| \ud83d\udd0c **Universal Compatibility** | Works with [Pydantic](https://pydantic.dev/), [dataclasses](https://docs.python.org/3/library/dataclasses.html), [TypedDict](https://docs.python.org/3/library/typing.html#typing.TypedDict), [attrs](https://www.attrs.org/), and plain objects |\n| \ud83c\udfaf **Flexible Policies** | Customize validation behavior (strict, lenient, coercive) |\n| \ud83c\udfd7\ufe0f **Protocol Integration** | Drop-in compatibility with Python's [typing.Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol) |\n| \ud83d\udd0d **ABC Support** | Use with `isinstance()` and `issubclass()` |\n| \ud83c\udfa8 **Ergonomic API** | Clean, intuitive interface with excellent IDE support |\n\n## \ud83d\udce6 Installation\n\n```bash\n# Basic installation\npip install duckdantic\n\n# With Pydantic support (recommended)\npip install \"duckdantic[pydantic]\"\n\n# Development installation\npip install \"duckdantic[all]\"\n```\n\n## \ud83d\ude80 Quick Start\n\n### The Duck API (Most Popular)\n\nPerfect for **Pydantic** users and modern Python development:\n\n```python\nfrom pydantic import BaseModel\nfrom duckdantic import Duck\n\n# Define your models\nclass User(BaseModel):\n    name: str\n    email: str\n    age: int\n    is_active: bool = True\n\nclass Person(BaseModel):\n    name: str\n    age: int\n\n# Create a structural type\nPersonShape = Duck(Person)\n\n# \u2705 Structural validation - no inheritance needed!\nuser = User(name=\"Alice\", email=\"alice@example.com\", age=30)\nassert isinstance(user, PersonShape)  # True! User has required Person fields\n\n# \ud83d\udd04 Convert between compatible types\nperson = PersonShape.convert(user)  # Person(name=\"Alice\", age=30)\n```\n\n### Universal Compatibility\n\nWorks with **any** Python object:\n\n```python\nfrom dataclasses import dataclass\nfrom typing import TypedDict\nfrom duckdantic import Duck\n\n# Works with dataclasses\n@dataclass\nclass DataPerson:\n    name: str\n    age: int\n\n# Works with TypedDict\nclass DictPerson(TypedDict):\n    name: str\n    age: int\n\n# Works with plain objects\nclass PlainPerson:\n    def __init__(self, name: str, age: int):\n        self.name = name\n        self.age = age\n\n# One duck type validates them all!\nPersonShape = Duck.from_fields({\"name\": str, \"age\": int})\n\n# \u2705 All of these work\nassert isinstance(DataPerson(\"Bob\", 25), PersonShape)\nassert isinstance(DictPerson(name=\"Charlie\", age=35), PersonShape)  \nassert isinstance(PlainPerson(\"Diana\", 28), PersonShape)\n```\n\n### Advanced: Trait-Based Validation\n\nFor complex structural requirements:\n\n```python\nfrom duckdantic import TraitSpec, FieldSpec, satisfies\n\n# Define a complex trait\nAPIResponse = TraitSpec(\n    name=\"APIResponse\",\n    fields=(\n        FieldSpec(\"status\", int, required=True),\n        FieldSpec(\"data\", dict, required=True),\n        FieldSpec(\"message\", str, required=False),\n        FieldSpec(\"timestamp\", str, required=False),\n    )\n)\n\n# Validate any object structure\nresponse1 = {\"status\": 200, \"data\": {\"users\": []}}\nresponse2 = {\"status\": 404, \"data\": {}, \"message\": \"Not found\"}\n\nassert satisfies(response1, APIResponse)  # \u2705 \nassert satisfies(response2, APIResponse)  # \u2705\n```\n\n### Method Validation\n\nEnsure objects implement required methods:\n\n```python\nfrom duckdantic import MethodSpec, methods_satisfy\n\n# Define method requirements (like typing.Protocol)\nDrawable = [\n    MethodSpec(\"draw\", params=[int, int], returns=None),\n    MethodSpec(\"get_bounds\", params=[], returns=tuple),\n]\n\nclass Circle:\n    def draw(self, x: int, y: int) -> None:\n        print(f\"Drawing circle at ({x}, {y})\")\n    \n    def get_bounds(self) -> tuple:\n        return (0, 0, 10, 10)\n\nassert methods_satisfy(Circle, Drawable)  # \u2705\n```\n\n## \ud83c\udfaf Common Use Cases\n\n### \ud83c\udf10 Microservices & APIs\n\n```python\nfrom duckdantic import Duck\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\n\nclass CreateUserRequest(BaseModel):\n    name: str\n    email: str\n\nclass UserResponse(BaseModel):\n    id: int\n    name: str\n    email: str\n    created_at: str\n\n# Ensure response compatibility across services\nResponseShape = Duck(UserResponse)\n\napp = FastAPI()\n\n@app.post(\"/users/\")\ndef create_user(request: CreateUserRequest):\n    # Any object with the right shape works\n    response = build_user_response(request)  # From any source\n    assert isinstance(response, ResponseShape)  # Validation\n    return response\n```\n\n### \ud83d\udd0c Plugin Systems\n\n```python\nfrom duckdantic import Duck\n\n# Define plugin interface\nclass PluginInterface:\n    name: str\n    version: str\n    def execute(self, data: dict) -> dict: ...\n\nPluginShape = Duck(PluginInterface)\n\n# Load plugins from anywhere\ndef load_plugin(plugin_class):\n    if isinstance(plugin_class(), PluginShape):\n        return plugin_class()\n    raise ValueError(\"Invalid plugin structure\")\n```\n\n### \ud83e\uddea Testing & Mocking\n\n```python\nfrom duckdantic import Duck\n\nProductionModel = Duck(YourProductionClass)\n\n# Create test doubles that satisfy production interfaces\nclass MockService:\n    def __init__(self):\n        self.required_field = \"test\"\n        self.another_field = 42\n\nmock = MockService()\nassert isinstance(mock, ProductionModel)  # \u2705 Valid test double\n```\n\n## \ud83d\udd17 Related Projects\n\n| Project | Relationship | When to Use |\n|---------|--------------|-------------|\n| [**Pydantic**](https://pydantic.dev/) | \ud83e\udd1d **Perfect Companion** | Use Pydantic for data validation, Duckdantic for structural typing |\n| [**typing.Protocol**](https://docs.python.org/3/library/typing.html#typing.Protocol) | \ud83d\udd04 **Runtime Alternative** | Protocols are static, Duckdantic is runtime + dynamic |\n| [**attrs**](https://www.attrs.org/) | \u2705 **Fully Compatible** | Define classes with attrs, validate with Duckdantic |\n| [**dataclasses**](https://docs.python.org/3/library/dataclasses.html) | \u2705 **Fully Compatible** | Built-in support for dataclass validation |\n| [**TypedDict**](https://docs.python.org/3/library/typing.html#typing.TypedDict) | \u2705 **Fully Compatible** | Validate dictionary structures dynamically |\n\n## \ud83d\udd2c Type Algebra & Relationships\n\nDuckdantic implements a **complete algebraic type system** with mathematical properties that enable powerful type composition and analysis:\n\n### Set-Theoretic Operations\n\n```python\nfrom duckdantic import Duck, TraitSpec, FieldSpec\n\n# Define base types\nPerson = Duck.from_fields({\"name\": str, \"age\": int})\nEmployee = Duck.from_fields({\"name\": str, \"age\": int, \"employee_id\": str})\nManager = Duck.from_fields({\"name\": str, \"age\": int, \"employee_id\": str, \"team\": list})\n\n# Subtyping relationships (Employee \u2286 Person)\nassert Employee <= Person  # Employee is a subtype of Person\nassert Manager <= Employee  # Manager is a subtype of Employee\nassert Manager <= Person   # Transitivity holds!\n\n# Type intersection (A \u2229 B)\nHasName = Duck.from_fields({\"name\": str})\nHasAge = Duck.from_fields({\"age\": int})\nPersonIntersect = HasName & HasAge  # Creates intersection type\n\n# Type union (A \u222a B)\nEmailUser = Duck.from_fields({\"email\": str})\nPhoneUser = Duck.from_fields({\"phone\": str})\nContactableUser = EmailUser | PhoneUser  # Either email OR phone\n\n# Type algebra satisfies mathematical laws\nassert (A <= B and B <= C) implies (A <= C)  # Transitivity\nassert (A & B) <= A and (A & B) <= B         # Intersection property\nassert A <= (A | B) and B <= (A | B)         # Union property\n```\n\n### Structural Width & Depth Subtyping\n\n```python\n# Width subtyping: More fields = more specific\nBasic = Duck.from_fields({\"id\": int})\nExtended = Duck.from_fields({\"id\": int, \"name\": str})\nassert Extended <= Basic  # Extended is a subtype (has more fields)\n\n# Depth subtyping: More specific field types\nGeneric = Duck.from_fields({\"items\": list})\nSpecific = Duck.from_fields({\"items\": list[str]})\nassert Specific <= Generic  # Specific is a subtype (more precise type)\n\n# Combined width + depth\nBaseAPI = Duck.from_fields({\"status\": int})\nDetailedAPI = Duck.from_fields({\"status\": int, \"data\": dict[str, Any], \"meta\": dict})\nassert DetailedAPI <= BaseAPI  # Satisfies both width and depth\n```\n\n### Algebraic Properties\n\nDuckdantic's type system satisfies important algebraic properties:\n\n| Property | Definition | Example |\n|----------|------------|---------|\n| **Reflexivity** | A \u2286 A | `Person <= Person` is always true |\n| **Antisymmetry** | A \u2286 B \u2227 B \u2286 A \u27f9 A = B | If types satisfy each other, they're equivalent |\n| **Transitivity** | A \u2286 B \u2227 B \u2286 C \u27f9 A \u2286 C | Subtyping chains work as expected |\n| **Join Existence** | \u2200A,B \u2203(A \u2228 B) | Union types always exist |\n| **Meet Existence** | \u2200A,B \u2203(A \u2227 B) | Intersection types always exist |\n\n### Practical Applications\n\n```python\n# Type-safe function composition\ndef process_person(obj: Person) -> dict:\n    \"\"\"Accepts any object satisfying Person shape\"\"\"\n    return {\"name\": obj.name.upper(), \"adult\": obj.age >= 18}\n\ndef process_employee(obj: Employee) -> dict:\n    \"\"\"More specific - requires employee_id too\"\"\"\n    result = process_person(obj)  # Safe! Employee <= Person\n    result[\"emp_id\"] = obj.employee_id\n    return result\n\n# Automatic type narrowing\nmanager = Manager(name=\"Alice\", age=35, employee_id=\"M001\", team=[\"Bob\", \"Charlie\"])\nassert isinstance(manager, Person)    # \u2705 True\nassert isinstance(manager, Employee)  # \u2705 True\nassert isinstance(manager, Manager)   # \u2705 True\n\n# Use in generic contexts\nfrom typing import TypeVar\n\nT = TypeVar('T', bound=Person)\n\ndef birthday(person: T) -> T:\n    \"\"\"Works with any Person-like type\"\"\"\n    person.age += 1\n    return person\n\n# Works with all subtypes!\nbirthday(manager)  # Manager in, Manager out\n```\n\n### Type Lattice Visualization\n\n```\n        \u22a4 (Top - Empty type)\n         |\n      Manager\n         |\n      Employee\n         |\n       Person\n         |\n    HasName \u2229 HasAge\n       /   \\\n   HasName  HasAge\n       \\   /\n        \u22a5 (Bottom - Any type)\n```\n\nThis algebraic foundation enables:\n- **Type inference**: Automatically determine most general valid types\n- **Type checking**: Mathematically prove type safety\n- **Type optimization**: Find minimal type representations\n- **Composition**: Build complex types from simple ones\n\n## \ud83c\udfd7\ufe0f Architecture Patterns\n\n### Hexagonal Architecture\n```python\n# Define port interfaces with Duck types\nUserRepositoryPort = Duck.from_methods({\n    \"save\": (User,) -> User,\n    \"find_by_id\": (int,) -> Optional[User],\n})\n\n# Any implementation that matches the structure works\nclass SQLUserRepository:\n    def save(self, user: User) -> User: ...\n    def find_by_id(self, user_id: int) -> Optional[User]: ...\n\nassert isinstance(SQLUserRepository(), UserRepositoryPort)  # \u2705\n```\n\n### CQRS Pattern\n```python\n# Command/Query interfaces as Duck types\nCommand = Duck.from_fields({\"command_id\": str, \"timestamp\": datetime})\nQuery = Duck.from_fields({\"query_id\": str, \"filters\": dict})\n\n# Any object with the right shape is valid\nCreateUserCommand = {\"command_id\": \"123\", \"timestamp\": datetime.now(), \"user_data\": {...}}\nassert isinstance(CreateUserCommand, Command)  # \u2705\n```\n\n## \ud83d\udd17 Type Relationships & Operators\n\nDuckdantic provides rich comparison operators for type analysis:\n\n### Comparison Operators\n\n```python\nfrom duckdantic import Duck\n\n# Define a type hierarchy\nAnimal = Duck.from_fields({\"name\": str})\nDog = Duck.from_fields({\"name\": str, \"breed\": str})\nPoodle = Duck.from_fields({\"name\": str, \"breed\": str, \"fluffy\": bool})\n\n# Subtype checking (<=, >=)\nassert Poodle <= Dog <= Animal  # Poodle is most specific\nassert Animal >= Dog >= Poodle  # Animal is most general\n\n# Strict subtype (<, >)\nassert Poodle < Dog < Animal   # Strictly more specific\nassert Animal > Dog > Poodle   # Strictly more general\n\n# Type equality (==)\nDog2 = Duck.from_fields({\"name\": str, \"breed\": str})\nassert Dog == Dog2  # Same structure = equal types\n\n# Type compatibility (~)\nclass MyDog:\n    name: str = \"Fido\"\n    breed: str = \"Labrador\"\n\nassert MyDog() ~ Dog  # MyDog instance satisfies Dog shape\n```\n\n### Advanced Type Operations\n\n```python\n# Type difference\nRequiredFields = Employee - Person  # Fields in Employee but not Person\n# Result: {\"employee_id\": str}\n\n# Type complement\nNotPerson = ~Person  # Types that don't satisfy Person\n\n# Conditional types\nAdult = Person.where(lambda p: p.age >= 18)\nSenior = Person.where(lambda p: p.age >= 65)\n\n# Type guards\ndef process_contact(contact: EmailUser | PhoneUser):\n    if contact ~ EmailUser:\n        send_email(contact.email)\n    elif contact ~ PhoneUser:\n        send_sms(contact.phone)\n```\n\n## \ud83d\udcca Performance\n\nDuckdantic is built for **production performance**:\n\n- **Intelligent caching**: Field analysis cached per type\n- **Lazy evaluation**: Only validates what's needed\n- **Zero-copy operations**: Minimal object creation overhead\n- **Optimized for common patterns**: Special handling for Pydantic/dataclasses\n\n```python\n# Benchmark: 1M validations\nimport timeit\nfrom duckdantic import Duck\n\nPersonShape = Duck.from_fields({\"name\": str, \"age\": int})\ntest_obj = {\"name\": \"test\", \"age\": 25}\n\ntime_taken = timeit.timeit(\n    lambda: isinstance(test_obj, PersonShape),\n    number=1_000_000\n)\nprint(f\"1M validations: {time_taken:.2f}s\")  # ~0.5s on modern hardware\n```\n\n## \ud83d\udee0\ufe0f Advanced Configuration\n\n### Custom Validation Policies\n\n```python\nfrom duckdantic import Duck, ValidationPolicy\n\n# Strict policy: exact type matching\nstrict_duck = Duck(MyModel, policy=ValidationPolicy.STRICT)\n\n# Lenient policy: duck typing with coercion\nlenient_duck = Duck(MyModel, policy=ValidationPolicy.LENIENT)\n\n# Custom policy: your own rules\nclass CustomPolicy(ValidationPolicy):\n    def validate_field(self, value, expected_type):\n        # Your custom validation logic\n        return custom_validation(value, expected_type)\n\ncustom_duck = Duck(MyModel, policy=CustomPolicy())\n```\n\n### Integration with Type Checkers\n\n```python\nfrom typing import TYPE_CHECKING\nfrom duckdantic import Duck\n\nif TYPE_CHECKING:\n    # Static type checking with Protocol\n    from typing import Protocol\n    \n    class PersonProtocol(Protocol):\n        name: str\n        age: int\nelse:\n    # Runtime validation with Duck\n    PersonProtocol = Duck.from_fields({\"name\": str, \"age\": int})\n\n# Works with both mypy and runtime!\ndef process_person(person: PersonProtocol) -> str:\n    return f\"{person.name} is {person.age} years old\"\n```\n\n## \ud83d\udcda Documentation\n\n| Resource | Description |\n|----------|-------------|\n| [\ud83d\udcd6 **Getting Started**](https://duckdantic.readthedocs.io/en/latest/getting-started/) | Complete setup and basic usage guide |\n| [\ud83c\udfaf **Examples**](https://duckdantic.readthedocs.io/en/latest/examples/) | Real-world usage patterns and recipes |\n| [\ud83d\udd27 **API Reference**](https://duckdantic.readthedocs.io/en/latest/api/) | Complete API documentation |\n| [\ud83c\udfd7\ufe0f **Architecture Guide**](https://duckdantic.readthedocs.io/en/latest/guide/architecture/) | Design patterns and best practices |\n| [\u26a1 **Performance Guide**](https://duckdantic.readthedocs.io/en/latest/guide/performance/) | Optimization tips and benchmarks |\n\n## \ud83e\udd1d Contributing\n\nWe love contributions! Duckdantic is built by the community, for the community.\n\n```bash\n# Quick setup\ngit clone https://github.com/pr1m8/duckdantic.git\ncd duckdantic\npip install -e \".[dev]\"\npytest  # Run tests\n```\n\nSee our [**Contributing Guide**](CONTRIBUTING.md) for detailed instructions.\n\n## \ud83c\udf89 Community\n\n- \ud83d\udc1b **Found a bug?** [Open an issue](https://github.com/pr1m8/duckdantic/issues)\n- \ud83d\udca1 **Have an idea?** [Start a discussion](https://github.com/pr1m8/duckdantic/discussions)\n- \ud83d\udcd6 **Need help?** Check our [documentation](https://duckdantic.readthedocs.io/)\n- \ud83c\udf1f **Like the project?** Give us a star on [GitHub](https://github.com/pr1m8/duckdantic)!\n- \ud83d\udce6 **Install it!** Get it from [PyPI](https://pypi.org/project/duckdantic/)\n\n## \ud83d\udcc4 License\n\nLicensed under the **MIT License** - see [LICENSE](LICENSE.txt) for details.\n\n## \ud83d\ude4f Acknowledgments\n\n- **TypeScript** and **Go interfaces** for structural typing inspiration\n- **[Pydantic](https://pydantic.dev/)** for showing how beautiful Python validation can be\n- **[Protocol](https://docs.python.org/3/library/typing.html#typing.Protocol)** for static structural typing patterns\n- **The Python community** for embracing duck typing as a core principle\n\n---\n\n<div align=\"center\">\n\n**\"In the face of ambiguity, refuse the temptation to guess.\"**  \n*\u2014 The Zen of Python*\n\n**Duckdantic: Where structure meets flexibility** \ud83e\udd86\u2728\n\n</div>",
    "bugtrack_url": null,
    "license": null,
    "summary": "Flexible structural typing and runtime validation for Python",
    "version": "1.1.1",
    "project_urls": {
        "Changelog": "https://github.com/pr1m8/duckdantic/blob/main/CHANGELOG.md",
        "Documentation": "https://duckdantic.readthedocs.io/",
        "Issues": "https://github.com/pr1m8/duckdantic/issues",
        "PyPI": "https://pypi.org/project/duckdantic/",
        "Repository": "https://github.com/pr1m8/duckdantic"
    },
    "split_keywords": [
        "duck-typing",
        " pydantic",
        " runtime-types",
        " structural-typing",
        " validation"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "1d6adfeb0051a94d9180ed4e170ae29320d2c5b726b49d7ae3ea6b0dee6dc930",
                "md5": "6a3660bfee9ea555d24f76fbef254d34",
                "sha256": "401e5aff66dfd0c9270e6c752e47a00682c7518535f12092e8416f15cd9b5aa6"
            },
            "downloads": -1,
            "filename": "duckdantic-1.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "6a3660bfee9ea555d24f76fbef254d34",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.12",
            "size": 35888,
            "upload_time": "2025-08-22T21:40:16",
            "upload_time_iso_8601": "2025-08-22T21:40:16.856626Z",
            "url": "https://files.pythonhosted.org/packages/1d/6a/dfeb0051a94d9180ed4e170ae29320d2c5b726b49d7ae3ea6b0dee6dc930/duckdantic-1.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "063633fbb60a525d254b54e7e4ca37bc77fd0de649fc65cee7ef556c415c0d65",
                "md5": "61d99f0582f72d540d314def59aef4a0",
                "sha256": "3ebe64d5347a50e2df17304d8b2315cfcddeabe6a0c959415d54fa8ba3f09f86"
            },
            "downloads": -1,
            "filename": "duckdantic-1.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "61d99f0582f72d540d314def59aef4a0",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.12",
            "size": 488269,
            "upload_time": "2025-08-22T21:40:18",
            "upload_time_iso_8601": "2025-08-22T21:40:18.330993Z",
            "url": "https://files.pythonhosted.org/packages/06/36/33fbb60a525d254b54e7e4ca37bc77fd0de649fc65cee7ef556c415c0d65/duckdantic-1.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-22 21:40:18",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "pr1m8",
    "github_project": "duckdantic",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "duckdantic"
}
        
Elapsed time: 1.67340s