CodeNebula
Back to Python Overview
🔧

Python — Core Language

Types, functions, classes, decorators, dataclasses

Python🔧Core Language

Variables and Types

Python is dynamically typed — a variable is just a name bound to an object; the type lives on the object, not the variable. Everything is an object: integers, strings, functions, classes. Python's built-in types include `int`, `float`, `complex`, `bool`, `str`, `bytes`, `list`, `tuple`, `dict`, `set`, `frozenset`, and `NoneType`. `type()` returns an object's type; `isinstance()` checks inheritance.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
x = 42            # int
y = 3.14          # float
z = 1 + 2j        # complex
s = "hello"       # str
b = b"bytes"      # bytes
t = (1, 2, 3)     # tuple (immutable)
lst = [1, 2, 3]   # list (mutable)
d = {"a": 1}      # dict
nothing = None    # NoneType

print(type(x), isinstance(x, int))   # <class 'int'> True
print(type(s), len(s))               # <class 'str'> 5
Python🔧Core Language

Control Flow

Python's control flow uses `if`/`elif`/`else`, `for` (iterating any iterable), `while`, `break`, `continue`, and `else` clauses on loops (the `else` runs if the loop wasn't broken out of). Conditional expressions (`x if cond else y`) provide ternary-style logic. Python has no `switch`; use `match`/`case` (3.10+) or dicts for dispatch tables.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
# for/else — runs else only if loop wasn't broken
def find_prime(n: int) -> bool:
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            break
    else:
        return True  # No divisor found
    return False

print([x for x in range(2, 20) if find_prime(x)])
# [2, 3, 5, 7, 11, 13, 17, 19]

# Conditional expression
score = 85
grade = "pass" if score >= 60 else "fail"
Python🔧Core Language

Functions — *args, **kwargs, and Defaults

Python functions support positional args, keyword args, defaults, variadic positional (`*args` → tuple), variadic keyword (`**kwargs` → dict), positional-only params (before `/`), and keyword-only params (after `*`). Functions are first-class: they can be passed, returned, and stored. The default value is evaluated once at definition time — a common gotcha with mutable defaults.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
def create_connection(host, port=5432, /, *, timeout=30, **options):
    # host, port: positional-only (before /)
    # timeout: keyword-only (after *)
    # options: extra keyword args
    print(f"Connecting to {host}:{port} (timeout={timeout})")
    print(f"Extra options: {options}")

create_connection("localhost", 5433, timeout=10, ssl=True, retry=3)

# *args and **kwargs forwarding
def wrapper(*args, **kwargs):
    print(f"args={args}, kwargs={kwargs}")
    return sum(args)

print(wrapper(1, 2, 3, label="sum"))
Python🔧Core Language

Lambda Functions

`lambda` creates an anonymous single-expression function. It's syntactically limited — no statements, no docstrings, no multiple expressions — by design. Lambdas are most useful as short callbacks to `sorted()`, `map()`, `filter()`, or `functools.reduce()`. For anything more complex, a named `def` is clearer.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
# Sort by multiple keys inline
employees = [
    {"name": "Alice", "dept": "eng", "salary": 120000},
    {"name": "Bob",   "dept": "eng", "salary": 95000},
    {"name": "Carol", "dept": "hr",  "salary": 80000},
]
ranked = sorted(employees, key=lambda e: (e["dept"], -e["salary"]))
for e in ranked:
    print(f"{e['dept']:5} {e['salary']:>7,}  {e['name']}")

# Lambda with map and filter
nums = range(1, 11)
evens_sq = list(map(lambda x: x**2, filter(lambda x: x % 2 == 0, nums)))
print(evens_sq)  # [4, 16, 36, 64, 100]
Python🔧Core Language

Comprehensions

Python has four comprehension forms: list `[expr for x in it if cond]`, dict `{k: v for ...}`, set `{expr for ...}`, and generator `(expr for ...)`. Comprehensions are faster than equivalent `for` loops (the loop variable is local, avoiding global lookup) and more readable. Nested comprehensions are legal but keep nesting shallow — beyond two levels, a named generator is clearer.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
# List, dict, set, generator comprehensions
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat   = [x for row in matrix for x in row]
evens  = {x for x in flat if x % 2 == 0}
sq_map = {x: x**2 for x in range(1, 6)}
gen    = (x**3 for x in range(1, 6))  # lazy

print(flat)              # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(evens)             # {2, 4, 6, 8}
print(sq_map)            # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
print(list(gen))         # [1, 8, 27, 64, 125]
Python🔧Core Language

Generators and the `yield` Keyword

A generator function uses `yield` instead of `return` — calling it returns a generator object (an iterator). Execution pauses at each `yield` and resumes on the next `next()` call. Generators are memory-efficient for large sequences: instead of building a full list in memory, values are produced on demand. `yield from` delegates to a sub-generator, enabling generator pipelines and recursive generators.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
import itertools

def fibonacci():
    """Infinite Fibonacci generator."""
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Take first 10 Fibonacci numbers
fibs = list(itertools.islice(fibonacci(), 10))
print(fibs)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

def read_in_chunks(file_obj, chunk_size=1024):
    """Generator that yields file content in fixed-size chunks."""
    while chunk := file_obj.read(chunk_size):
        yield chunk
Python🔧Core Language

Decorators

A decorator is a callable that takes a function and returns a (usually modified) function. The `@decorator` syntax is syntactic sugar for `func = decorator(func)`. Decorators are used for cross-cutting concerns: logging, timing, access control, caching, retry logic, and input validation — without modifying the decorated function's body. `functools.wraps` preserves the wrapped function's metadata (`__name__`, `__doc__`).

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
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

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}. Retrying...")
        return wrapper
    return decorator

@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> str:
    return f"data from {url}"

print(fetch_data("https://example.com"))
Python🔧Core Language

Classes and Object-Oriented Programming

Python uses class-based OOP with dynamic dispatch. `__init__` is the initializer (not a constructor — `__new__` allocates the object). Instance attributes are set on `self`; class attributes are shared across instances. `@classmethod` receives the class as first arg; `@staticmethod` receives neither. Properties (`@property`) give attribute-style access to computed values.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
class BankAccount:
    interest_rate = 0.02  # class attribute

    def __init__(self, owner: str, balance: float = 0.0):
        self._owner = owner
        self._balance = balance

    @property
    def balance(self) -> float:
        return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self._balance += amount

    def withdraw(self, amount: float) -> None:
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount

    @classmethod
    def from_dict(cls, data: dict) -> "BankAccount":
        return cls(data["owner"], data.get("balance", 0.0))

    def __repr__(self) -> str:
        return f"BankAccount(owner={self._owner!r}, balance={self._balance:.2f})"

acc = BankAccount.from_dict({"owner": "Alice", "balance": 1000})
acc.deposit(500)
acc.withdraw(200)
print(acc)  # BankAccount(owner='Alice', balance=1300.00)
Python🔧Core Language

Inheritance and MRO

Python supports multiple inheritance. The Method Resolution Order (MRO) determines which class's method is called when multiple bases define the same name. Python uses the C3 linearization algorithm — `ClassName.__mro__` shows the order. `super()` follows the MRO, making cooperative multiple inheritance possible (all `__init__` methods in the hierarchy can be called in sequence using `super()`).

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
class A:
    def who(self): return "A"

class B(A):
    def who(self): return f"B -> {super().who()}"

class C(A):
    def who(self): return f"C -> {super().who()}"

class D(B, C):
    def who(self): return f"D -> {super().who()}"

d = D()
print(d.who())      # D -> B -> C -> A
print(D.__mro__)    # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Python🔧Core Language

Dunder (Magic) Methods

Dunder methods (`__method__`) let classes integrate with Python's built-in operators and protocols. `__repr__` / `__str__` control string representation. `__len__`, `__getitem__`, `__setitem__`, `__iter__` implement the sequence and mapping protocols. `__enter__` / `__exit__` enable the `with` statement. `__eq__`, `__lt__`, `__hash__` control equality and hashing. `__call__` makes instances callable.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
class Vector:
    def __init__(self, *components):
        self._data = list(components)

    def __repr__(self): return f"Vector{tuple(self._data)}"
    def __len__(self):  return len(self._data)
    def __getitem__(self, i): return self._data[i]
    def __add__(self, other): return Vector(*(a + b for a, b in zip(self, other)))
    def __mul__(self, scalar): return Vector(*(x * scalar for x in self))
    def __eq__(self, other):  return self._data == list(other)

v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)
print(v1 + v2)    # Vector(5, 7, 9)
print(v1 * 3)     # Vector(3, 6, 9)
print(len(v1))    # 3
print(v1[1])      # 2
Python🔧Core Language

Context Managers and `with`

The `with` statement guarantees cleanup code runs even if exceptions occur. Any object implementing `__enter__` / `__exit__` is a context manager. The `contextlib.contextmanager` decorator lets you write generator-based context managers — `yield` is the body of the `with` block. `contextlib.ExitStack` composes a dynamic number of context managers.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
import contextlib
import time

@contextlib.contextmanager
def timed(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.4f}s")

@contextlib.contextmanager
def managed_resource(name: str):
    print(f"Acquiring {name}")
    try:
        yield name.upper()
    finally:
        print(f"Releasing {name}")

with timed("demo"):
    with managed_resource("database") as res:
        print(f"Using {res}")
Python🔧Core Language

Exception Handling

Python exceptions are objects that inherit from `BaseException`. The hierarchy: `BaseException` → `Exception` → specific exceptions like `ValueError`, `TypeError`, `IOError`. The `try`/`except`/`else`/`finally` structure: `except` catches errors, `else` runs only if no exception occurred, `finally` always runs. Python 3.11+ added `ExceptionGroup` for handling multiple simultaneous exceptions (from `asyncio.TaskGroup`).

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
class InsufficientFundsError(ValueError):
    def __init__(self, balance: float, amount: float):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Need {amount:.2f} but only have {balance:.2f}")

def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        return None
    except TypeError as e:
        raise ValueError(f"Arguments must be numbers: {e}") from e
    else:
        return result
    finally:
        pass  # cleanup always runs

print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))   # None

try:
    raise InsufficientFundsError(50.0, 100.0)
except InsufficientFundsError as e:
    print(e)  # Need 100.00 but only have 50.00
Python🔧Core Language

Iterables, Iterators, and the Iterator Protocol

Python's `for` loop works on any *iterable* — an object that implements `__iter__()` returning an *iterator*. An iterator implements `__next__()` raising `StopIteration` when exhausted. Built-ins like `list`, `dict`, `str`, `range`, files, and generators are all iterables. `iter(obj)` and `next(obj)` are the protocol functions. Understanding this protocol explains how `for`, comprehensions, `zip`, `map`, `filter`, and `itertools` all interoperate.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
class CountUp:
    """Iterator that counts from start to stop."""
    def __init__(self, start: int, stop: int):
        self._current = start
        self._stop = stop

    def __iter__(self):
        return self  # Iterator is its own iterable

    def __next__(self):
        if self._current >= self._stop:
            raise StopIteration
        val = self._current
        self._current += 1
        return val

for n in CountUp(1, 6):
    print(n, end=" ")  # 1 2 3 4 5

# Equivalent generator expression
gen = (n for n in range(1, 6))
print(list(gen))  # [1, 2, 3, 4, 5]
Python🔧Core Language

Type Hints (PEP 484)

Python 3.5 introduced type hints via PEP 484. They are not enforced at runtime — they're metadata for static type checkers (`mypy`, `pyright`, `ruff`). The `typing` module provides `List`, `Dict`, `Optional`, `Union`, `Callable`, `TypeVar`, `Generic`, `Protocol`. Python 3.10+ replaced `Optional[X]` with `X | None` and `Union[X, Y]` with `X | Y`. Python 3.12 added `type` statement for type aliases.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
from typing import TypeVar, Generic, Callable
from collections.abc import Iterable

T = TypeVar("T")
U = TypeVar("U")

def first(items: Iterable[T]) -> T | None:
    return next(iter(items), None)

def transform(items: list[T], fn: Callable[[T], U]) -> list[U]:
    return [fn(item) for item in items]

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

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

    def pop(self) -> T:
        return self._items.pop()

stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop())  # 2
print(first([10, 20, 30]))  # 10
print(transform(["a", "bb", "ccc"], len))  # [1, 2, 3]
Python🔧Core Language

Walrus Operator `:=` (Python 3.8+)

The walrus operator (`:=`, PEP 572) assigns a value to a variable and returns it in a single expression. It's most useful inside `while` conditions (avoiding a duplicate read), comprehension conditions (capturing an intermediate computed value), and `if` statements (avoiding re-computation). It makes imperative "compute, check, use" patterns more concise without sacrificing readability when used judiciously.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
import re

# Walrus in a while loop — read chunks until EOF
def count_chars(text: str, chunk_size: int = 4) -> int:
    total = 0
    pos = 0
    while chunk := text[pos:pos + chunk_size]:
        total += len(chunk)
        pos += chunk_size
    return total

print(count_chars("Hello, World!"))  # 13

# Walrus in comprehension — avoid recomputing expensive fn
data = [1, -2, 3, -4, 5]
results = [y for x in data if (y := x * x) > 4]
print(results)  # [4, 9, 16, 25] — wait, condition is y > 4
# => [9, 16, 25]

# Walrus in regex match
text = "Order #12345 placed"
if m := re.search(r"#(\d+)", text):
    print(f"Order ID: {m.group(1)}")  # Order ID: 12345
Python🔧Core Language

Structural Pattern Matching (`match`/`case`, Python 3.10+)

`match`/`case` (PEP 634) is not a simple switch — it performs structural decomposition. Patterns can match literals, variable capture (`case Point(x, y):`), sequences (`case [first, *rest]:`), mappings (`case {"action": action}:`), class patterns, OR patterns (`|`), and guards (`if condition`). Captured variables are bound in the case body. The `_` wildcard matches anything without binding.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

def classify_point(p):
    match p:
        case Point(x=0, y=0):
            return "origin"
        case Point(x=0, y=y):
            return f"on y-axis at {y}"
        case Point(x=x, y=0):
            return f"on x-axis at {x}"
        case Point(x=x, y=y) if x == y:
            return f"on diagonal at {x}"
        case Point(x=x, y=y):
            return f"point ({x}, {y})"

for pt in [Point(0, 0), Point(0, 5), Point(3, 0), Point(4, 4), Point(1, 2)]:
    print(classify_point(pt))
Python🔧Core Language

Dataclasses (Python 3.7+)

`@dataclass` auto-generates `__init__`, `__repr__`, `__eq__` (and optionally `__lt__`, `__hash__`, `__post_init__`) from class-level annotations. With `frozen=True` the class is immutable (and hashable). `field()` controls defaults, whether a field appears in `__repr__`, and `default_factory` for mutable defaults. Python 3.10 added `slots=True` for automatic `__slots__` (lower memory, faster attribute access).

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
from dataclasses import dataclass, field
from typing import ClassVar

@dataclass(order=True, frozen=True)
class Version:
    major: int
    minor: int
    patch: int = 0
    _registry: ClassVar[list] = []  # ClassVar excluded from __init__

    def __str__(self) -> str:
        return f"{self.major}.{self.minor}.{self.patch}"

@dataclass
class Package:
    name: str
    version: Version
    dependencies: list[str] = field(default_factory=list)
    _internal: str = field(default="", repr=False)

v1 = Version(3, 11)
v2 = Version(3, 12)
print(v1 < v2)   # True (order=True generates __lt__)
print(str(v1))   # 3.11.0

pkg = Package("requests", v2, ["certifi", "urllib3"])
print(pkg)
# Package(name='requests', version=3.12.0, dependencies=['certifi', 'urllib3'])
Python🔧Core Language

f-Strings and String Formatting

f-strings (PEP 498, Python 3.6+) embed expressions in string literals: `f"{expr}"`. They call `__format__` on the object with an optional format spec after `:`. Python 3.12 removed the restriction that f-string expressions couldn't contain the same quote type as the outer string. `=` suffix (`f"{value=}"`) prints both the expression text and its value — a debug shorthand.

beginnerflip for code
1
2
3
4
5
6
7
8
9
10
import math
from datetime import datetime

# Format spec: [[fill]align][sign][#][0][width][grouping][.prec][type]
pi      = math.pi
salary  = 123456.789
now     = datetime.now()

print(f"{pi:.4f}")            # 3.1416
print(f"{salary:>15,.2f}")    # 123,456.79 (right-aligned, 15 wide)
print(f"{42:#010b}")          # 0b00101010 (binary with prefix, 10 wide)
print(f"{now:%Y-%m-%d}")      # 2024-01-15 (date format)

# Debug shorthand (Python 3.8+)
x = [1, 2, 3]
print(f"{x=}")                # x=[1, 2, 3]

# Nested f-strings and Python 3.12 same-quote expressions
items = ["a", "b", "c"]
print(f"{'|'.join(items)}")   # a|b|c (Python 3.12+: same quote inside)