Skip to content
Code Kata
Go back

Python Core Concepts

Suggest edit

Table of contents

Open Table of contents

Overview

Python’s power lies in its consistent data model and composable abstractions. This reference covers the core concepts you’ll encounter throughout the kata exercises.

The Data Model (Dunder Methods)

Python objects communicate through special (“dunder”) methods. Implementing them makes your objects work with built-in operations.

class Vector:
    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other: "Vector") -> "Vector":
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:
        return hash((self.x, self.y))

    def __abs__(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

Key Dunder Methods for Data Structures

MethodTriggered ByUse Case
__len__len(obj)Collection size
__getitem__obj[key]Index/key access
__setitem__obj[key] = valIndex/key assignment
__contains__x in objMembership test
__iter__for x in objIteration
__next__next(obj)Iterator protocol
__eq__, __lt__, etc.==, <Comparison
__hash__hash(obj)Dict keys, set members

Decorators

Functions that wrap other functions or classes:

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)

Decorators with Arguments

def retry(max_attempts: int = 3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == max_attempts - 1:
                        raise
        return wrapper
    return decorator

@retry(max_attempts=5)
def fetch_data(url: str) -> dict:
    ...

Class Decorators

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

Generators and Iterators

The Iterator Protocol

Any object implementing __iter__ and __next__:

class Range:
    def __init__(self, start: int, stop: int) -> None:
        self.start = start
        self.stop = stop

    def __iter__(self):
        current = self.start
        while current < self.stop:
            yield current
            current += 1

Generator Functions

Lazy evaluation with yield:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Take first 10
from itertools import islice
first_10 = list(islice(fibonacci(), 10))

Generator Expressions

squares = (x ** 2 for x in range(1_000_000))  # lazy, no list in memory
total = sum(x ** 2 for x in range(1_000_000))  # direct to sum

Context Managers

Manage setup/teardown with with statements:

from contextlib import contextmanager

@contextmanager
def database_transaction(db):
    tx = db.begin()
    try:
        yield tx
        tx.commit()
    except Exception:
        tx.rollback()
        raise

with database_transaction(db) as tx:
    tx.execute("INSERT INTO ...")

Class-Based Context Manager

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self.start
        return False  # don't suppress exceptions

with Timer() as t:
    do_work()
print(f"Elapsed: {t.elapsed:.4f}s")

Type Hints

Basic Annotations

from collections.abc import Sequence

def process_items(items: Sequence[str], limit: int = 10) -> list[str]:
    return [item.upper() for item in items[:limit]]

Generics (Python 3.12+)

class Stack[T]:
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

Protocols (Structural Typing)

Duck typing with type safety:

from typing import Protocol

class Comparable(Protocol):
    def __lt__(self, other: "Comparable") -> bool: ...

def find_min[T: Comparable](items: list[T]) -> T:
    return min(items)

TypedDict

from typing import TypedDict

class UserData(TypedDict):
    name: str
    email: str
    age: int

The collections Module

defaultdict

from collections import defaultdict

graph: defaultdict[str, list[str]] = defaultdict(list)
graph["a"].append("b")
graph["a"].append("c")
# No KeyError -- missing keys get default value

deque

Double-ended queue with O(1) operations at both ends:

from collections import deque

d: deque[int] = deque([1, 2, 3])
d.appendleft(0)  # O(1)
d.pop()           # O(1)
d.popleft()       # O(1)

# Bounded deque (sliding window)
recent: deque[str] = deque(maxlen=5)

Counter

from collections import Counter

words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts.most_common(2))  # [('apple', 3), ('banana', 2)]

namedtuple and NamedTuple

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

p = Point(1.0, 2.0)
x, y = p  # unpacking works

Comprehensions

List, Dict, Set

# List
squares = [x ** 2 for x in range(10) if x % 2 == 0]

# Dict
word_lengths = {word: len(word) for word in ["hello", "world"]}

# Set
unique_chars = {c for word in words for c in word}

Nested Comprehensions

# Flatten a matrix
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]

dataclasses

from dataclasses import dataclass, field

@dataclass
class TreeNode:
    value: int
    children: list["TreeNode"] = field(default_factory=list)

    def add_child(self, child: "TreeNode") -> None:
        self.children.append(child)

Frozen Dataclasses (Immutable)

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

functools Highlights

from functools import lru_cache, reduce, partial

# Memoization
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# Reduce
total = reduce(lambda a, b: a + b, [1, 2, 3, 4])  # 10

# Partial application
from operator import mul
double = partial(mul, 2)

Key Takeaways

  1. Dunder methods — make your objects work with Python’s built-in operations
  2. Generators — lazy evaluation is idiomatic Python
  3. Protocols — structural typing (duck typing with type safety)
  4. collectionsdefaultdict, deque, Counter solve common problems efficiently
  5. Dataclasses — the default choice for data containers

Suggest edit
Share this post on:

Previous Post
Kata: Implement a Linked List
Next Post
Python 3.10+: Key Updates by Version