---
paths:
  - "*/src/**/*.py"
  - "*/tests/**/*.py"
---

# Type & Exception Patterns

Type annotations, exception design, and immutable update patterns for codeflash packages.

## Type Aliases

```python
from typing import TypeAlias

Processor: TypeAlias = Callable[
    [WrappedLogger, str, EventDict], EventDict | str | bytes
]
BackoffHook: TypeAlias = Callable[[Exception], bool | float | dt.timedelta]
```

Name complex callable signatures. The alias is documentation.

## Overloads for Return Type Variation

```python
@overload
def get_logger(**initial_values: Any) -> BoundLogger: ...

@overload
def get_logger(
    **initial_values: Any,
) -> FilteringBoundLogger: ...

def get_logger(**initial_values: Any) -> Any:
    """
    Create a new logger.
    """
    ...
```

Use `@overload` when the return type depends on arguments. The implementation signature uses `Any`.

## Protocols for Structural Typing

```python
@runtime_checkable
class HasClose(Protocol):
    def close(self) -> None: ...

class HasAsyncClose(Protocol):
    async def aclose(self) -> None: ...
```

Prefer Protocol over ABC when you want structural subtyping. Use `@runtime_checkable` only when isinstance checks are needed.

## Version-Gated Types

```python
import sys

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self

if sys.version_info >= (3, 10):
    from typing import TypeAlias
else:
    from typing_extensions import TypeAlias
```

Always use `sys.version_info` checks, never `try/except ImportError`, for version-gated standard library features. The version check is explicit, greppable, and unambiguous.

## Exception Patterns

### Simple, Focused Classes

```python
class ServiceNotFoundError(Exception):
    """
    Raised when a service type is not registered with the container.
    """


class MissingEnvValueError(Exception):
    """
    Raised when an environment variable is missing and no default is set.

    .. attribute:: var_name

       The name of the missing environment variable.
    """

    def __init__(self, var_name: str) -> None:
        self.var_name = var_name
        super().__init__(f"Environment variable '{var_name}' is not set.")
```

One class per failure mode. Docstring explains when the user will see this. Attribute documentation in RST format when the exception carries data.

### Chaining

```python
try:
    value = os.environ[key]
except KeyError as e:
    raise MissingEnvValueError(key) from e
```

Always chain with `from`. Never `raise X` alone when you caught something — the original traceback is valuable.

## The Bind Pattern (Immutable Updates)

A recurring pattern for immutable objects that need "mutation":

```python
@attrs.frozen
class BoundLogger:
    _context: dict[str, Any] = attrs.Factory(dict)

    def bind(self, **new_values: Any) -> Self:
        """
        Return a new logger with *new_values* added to the existing ones.
        """
        new_context = {**self._context, **new_values}

        return self.__class__(
            self._logger,
            self._processors,
            new_context,
        )

    def unbind(self, *keys: str) -> Self:
        """
        Return a new logger with *keys* removed from the context.
        """
        new_context = {k: v for k, v in self._context.items() if k not in keys}

        return self.__class__(
            self._logger,
            self._processors,
            new_context,
        )
```

Never mutate. Return new instances. The caller decides what to do with the new object.

## Context Manager Pattern

```python
@attrs.define
class Container:
    _cleanups: list[Callable] = attrs.Factory(list)

    def __enter__(self) -> Self:
        return self

    def __exit__(self, *exc_info: object) -> None:
        self.close()

    async def __aenter__(self) -> Self:
        return self

    async def __aexit__(self, *exc_info: object) -> None:
        await self.aclose()

    def close(self) -> None:
        """
        Run all registered cleanups.
        """
        for cleanup in reversed(self._cleanups):
            cleanup()
        self._cleanups.clear()
```

Dual sync/async context manager support. Cleanup runs in reverse registration order. The `close()`/`aclose()` methods are public so they can be called outside of `with` blocks.
