Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions 53.typing_protocols_and_generics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Small, commented examples: Protocols, Generics and ParamSpec.

Keep this file short and runnable. Intended as a quick demo for contributors.
"""

from typing import Protocol, runtime_checkable, TypeVar, Generic, Callable, ParamSpec

P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")


@runtime_checkable
class SupportsClose(Protocol):
"""Structural type: any object with a close() method fits."""
def close(self) -> None: ...


class Socket:
"""Simple object that implements close()."""
def __init__(self, name: str) -> None:
self.name = name

def close(self) -> None:
print(f"Socket {self.name} closed")


def close_if_supported(obj: SupportsClose) -> None:
"""Call .close() on anything that matches the Protocol.

This shows structural typing: no inheritance required.
"""
obj.close()


class Box(Generic[T]):
"""Tiny generic container.

Static type checkers will remember the element type.
"""
def __init__(self, value: T) -> None:
self._v = value

def get(self) -> T:
return self._v


# ParamSpec decorator: preserves caller signature for typing tools.
def make_logged(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore[misc]
print(f"[log] {func.__name__} args={args} kwargs={kwargs}")
res = func(*args, **kwargs)
print(f"[log] {func.__name__} -> {res}")
return res

return wrapper


@make_logged
def greet(name: str, excited: bool = False) -> str:
return "Hello, " + name + ("!!!" if excited else ".")


if __name__ == "__main__":
# Protocol demo
s = Socket("A")
close_if_supported(s)

# Generic demo
bi = Box[int](10)
bs = Box[str]("ok")
print(type(bi.get()), bi.get())
print(type(bs.get()), bs.get())

# ParamSpec demo (runtime logs and preserved signature for mypy)
print(greet("World"))
print(greet("Lin", excited=True))
87 changes: 87 additions & 0 deletions 54.descriptors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Short descriptor examples with human-like comments.

Shows a Typed data descriptor and a simple cached property.
"""

from typing import Any


class Typed:
"""Data descriptor that enforces the value type.

We store the value in the instance.__dict__ under the provided name.
This raises on wrong types so callers get early feedback.
"""

def __init__(self, name: str, expected_type: type) -> None:
self.name = name
self.expected_type = expected_type

def __set_name__(self, owner, name):
# saves the attribute name if the class assigns a different one
self.public_name = name

def __get__(self, instance: Any, owner: type = None) -> Any:
# when accessed on the class, return the descriptor itself
if instance is None:
return self
return instance.__dict__.get(self.name)

def __set__(self, instance: Any, value: Any) -> None:
# enforce the expected type
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} must be {self.expected_type.__name__}")
instance.__dict__[self.name] = value


class CachedProperty:
"""A tiny cached-property: compute once, then store on the instance.

This is a non-data descriptor (no __set__), so assigning the cached name
on the instance bypasses future descriptor lookups.
"""

def __init__(self, func):
self.func = func
self.__doc__ = getattr(func, '__doc__')

def __get__(self, instance, owner=None):
if instance is None:
return self
value = self.func(instance)
instance.__dict__[self.func.__name__] = value
return value


class Point:
# descriptors: declare at the class level
x = Typed('x', int)
y = Typed('y', int)

def __init__(self, x: int, y: int) -> None:
# these assignments go through Typed.__set__
self.x = x
self.y = y

@CachedProperty
def hypot(self):
# small computation that we'd like to cache
print('computing hypot')
from math import hypot
return hypot(self.x, self.y)


if __name__ == '__main__':
p = Point(3, 4)
print('x, y:', p.x, p.y)

# wrong type assignment shows the guard
try:
p.x = 2.5
except TypeError as e:
print('caught type error:', e)

# cached property: first access computes, second returns cached value
print('first hypot:', p.hypot)
print('second hypot (cached):', p.hypot)
print('instance keys:', list(p.__dict__.keys()))
37 changes: 37 additions & 0 deletions handbook/50_main_concepts_53_54.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Advanced Python Interview Reference

## Concepts 53–54 (Senior-Level)

---

## 53 — Typing: Protocols & Generics


Protocols and generics are tools from Python’s static typing system that allow you to write flexible APIs without losing type safety.

Common use cases:

- Polymorphic interfaces (duck typing)
- Flexible function/type arguments
- Safer, clearer APIs

**Related Examples**
- [`53.typing_protocols_and_generics.py`](../53.typing_protocols_and_generics.py)

---

## 54 — Descriptors (Typed + CachedProperty)

Descriptors let you control how attributes are accessed and stored on objects. They're used to add type enforcement, lazy computation, or caching to attribute access.

Common use cases:

- Type validation on assignment
- Cached/computed properties (like @property but cached)
- Logging or auditing attribute changes
- Read-only or write-only attributes
- Custom attribute behavior in libraries and frameworks


**Related Examples**
- [`54.descriptors.py`](../54.descriptors.py)