All topics
Data · Learning hub

Python notes for developers

Master Python with a curated set of 4 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Data notes
Python

Core Language

Python Core Language Python is a high-level, interpreted language valued for its readability, versatility, and rich standard library. It powers web development,

Python Core Language

Python is a high-level, interpreted language valued for its readability, versatility, and rich standard library. It powers web development, data science, scripting, automation, and machine learning.

Variables & Built-in Types

# Numbers
x: int = 42
y: float = 3.14
z: complex = 2 + 3j

# Strings
name = "Alice"
upper = name.upper()       # 'ALICE'
sliced = name[1:4]         # 'lic'
f_str = f"Hello, {name}! Age: {30}"

# Booleans
is_valid: bool = True

# None
result = None
print(result is None)      # True

# Type checking
print(type(42))            # <class 'int'>
print(isinstance(42, int)) # True

Collections

# List — mutable, ordered, allows duplicates
fruits = ['apple', 'banana', 'cherry']
fruits.append('date')
fruits.sort()
sliced = fruits[1:3]

# Tuple — immutable, ordered
point = (10, 20)
x, y = point               # unpacking

# Dictionary — key-value pairs
user = {'name': 'Alice', 'age': 30}
user['email'] = 'alice@example.com'
name = user.get('name', 'Anonymous')
for key, value in user.items():
    print(f"{key}: {value}")

# Set — unordered, unique
unique = {1, 2, 3, 2}     # {1, 2, 3}
evens = {2, 4, 6}
odds = {1, 3, 5}
union = evens | odds
intersection = evens & {2, 4, 8}

Control Flow

# Conditional expression (ternary)
score = 85
grade = 'A' if score >= 90 else 'B' if score >= 80 else 'C'

# for with enumerate and zip
items = ['a', 'b', 'c']
for i, item in enumerate(items, start=1):
    print(f"{i}: {item}")

keys = ['name', 'age']
values = ['Alice', 30]
for k, v in zip(keys, values):
    print(f"{k} = {v}")

# while with else
count = 0
while count < 5:
    count += 1
else:
    print("Completed normally")

# match statement (Python 3.10+)
command = "quit"
match command:
    case "quit":
        print("Quitting")
    case "help":
        print("Help menu")
    case _:
        print(f"Unknown: {command}")

Functions

from typing import Optional

# Type annotations and defaults
def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

# *args and **kwargs
def log(*messages: str, level: str = "INFO") -> None:
    for msg in messages:
        print(f"[{level}] {msg}")

log("Starting", "Ready", level="DEBUG")

# Positional-only (/) and keyword-only (*) parameters
def create_user(name: str, /, *, role: str = "user") -> dict:
    return {"name": name, "role": role}

# Closures
def make_counter(start: int = 0):
    count = start
    def increment(step: int = 1) -> int:
        nonlocal count
        count += step
        return count
    return increment

counter = make_counter(10)
print(counter())    # 11
print(counter(5))   # 16

Comprehensions

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

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

# Set comprehension
unique_lengths = {len(w) for w in ['cat', 'dog', 'elephant', 'ant']}

# Generator expression — lazy, O(1) memory
total = sum(x**2 for x in range(1_000_000))

# Nested (matrix transpose)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
transposed = [[row[i] for row in matrix] for i in range(3)]

Error Handling

try:
    result = 10 / int(input("Enter number: "))
except ZeroDivisionError:
    print("Cannot divide by zero")
except ValueError as e:
    print(f"Invalid input: {e}")
except (TypeError, OverflowError) as e:
    print(f"Math error: {e}")
else:
    print(f"Result: {result}")  # only runs if no exception
finally:
    print("Always runs")

# Custom exceptions
class AppError(Exception):
    def __init__(self, message: str, code: int = 500):
        super().__init__(message)
        self.code = code

class NotFoundError(AppError):
    def __init__(self, resource: str):
        super().__init__(f"{resource} not found", code=404)

# Exception chaining
try:
    data = open("config.json")
except FileNotFoundError as e:
    raise AppError("Configuration missing") from e
Python

OOP & Advanced Python

OOP & Advanced Python Python supports object-oriented programming with classes, special methods, and inheritance. Advanced features — decorators, generators, co

OOP & Advanced Python

Python supports object-oriented programming with classes, special methods, and inheritance. Advanced features — decorators, generators, context managers, and dataclasses — enable elegant and reusable code.

Classes & Special Methods

class Vector:
    def __init__(self, x: float, y: float):
        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 __mul__(self, scalar: float) -> 'Vector':
        return Vector(self.x * scalar, self.y * scalar)

    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))

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

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)         # Vector(4, 6)
print(v1.magnitude)    # 5.0

Inheritance & Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    def describe(self) -> str:
        return (f"{type(self).__name__}: "
                f"area={self.area():.2f}, "
                f"perimeter={self.perimeter():.2f}")

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

    def perimeter(self) -> float:
        return 2 * 3.14159 * self.radius

class Rectangle(Shape):
    def __init__(self, w: float, h: float):
        self.w = w
        self.h = h

    def area(self) -> float:        return self.w * self.h
    def perimeter(self) -> float:   return 2 * (self.w + self.h)

shapes: list[Shape] = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.describe())

Dataclasses

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass(frozen=True)    # immutable and hashable
class Point:
    x: float
    y: float

    def distance_to(self, other: 'Point') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

@dataclass
class Team:
    name: str
    max_size: ClassVar[int] = 10  # class variable, not a field
    members: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.name = self.name.strip().title()

    def add_member(self, name: str) -> None:
        if len(self.members) >= self.max_size:
            raise ValueError("Team is full")
        self.members.append(name)

team = Team("  dev team  ")
team.add_member("Alice")
print(team)  # Team(name='Dev Team', members=['Alice'])

Decorators

import functools, time

# Basic decorator
def timer(func):
    @functools.wraps(func)    # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__!r}: {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

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

@timer
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    return {"status": "ok"}

Generators

import itertools

# Generator function — yields values lazily
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

first_10 = list(itertools.islice(fibonacci(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# yield from — delegate to sub-generator
def chain_files(*paths):
    for path in paths:
        with open(path) as f:
            yield from f   # yields each line

# Generator pipeline (memory-efficient data processing)
def read_csv(path):
    with open(path) as f:
        yield from f

def parse_rows(lines):
    return (line.strip().split(',') for line in lines)

def filter_active(rows):
    return (row for row in rows if row[2] == 'active')

# Compose the pipeline
pipeline = filter_active(parse_rows(read_csv("users.csv")))

Context Managers

from contextlib import contextmanager, suppress

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

with timer_context("query"):
    time.sleep(0.05)

# Class-based context manager
class Transaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.conn.begin()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.conn.rollback()
        else:
            self.conn.commit()
        return False   # don't suppress exceptions

# suppress — silently ignore specific exceptions
with suppress(FileNotFoundError):
    open('optional.txt')  # won't raise if missing
Python

Async Programming & Ecosystem

Async Programming & Python Ecosystem Python's asyncio enables high-concurrency I/O without threads. Combined with its rich ecosystem — aiohttp, SQLAlchemy, Cele

Async Programming & Python Ecosystem

Python's asyncio enables high-concurrency I/O without threads. Combined with its rich ecosystem — aiohttp, SQLAlchemy, Celery, and more — Python excels at building scalable web services and data pipelines.

asyncio Fundamentals

import asyncio

async def fetch(url: str, delay: float = 1.0) -> str:
    await asyncio.sleep(delay)   # non-blocking I/O simulation
    return f"Data from {url}"

async def main():
    # Sequential — total: 3 seconds
    r1 = await fetch("api1.com", 1.0)
    r2 = await fetch("api2.com", 2.0)

    # Concurrent with gather — total: 2 seconds (limited by slowest)
    results = await asyncio.gather(
        fetch("api1.com", 1.0),
        fetch("api2.com", 2.0),
        fetch("api3.com", 0.5),
    )
    print(results)

    # Ignore individual errors
    results = await asyncio.gather(
        fetch("api1.com"),
        fetch("broken.com"),
        return_exceptions=True,   # exceptions returned, not raised
    )

asyncio.run(main())

Real HTTP with aiohttp

import aiohttp
import asyncio

async def fetch_json(session: aiohttp.ClientSession, url: str) -> dict:
    timeout = aiohttp.ClientTimeout(total=10)
    async with session.get(url, timeout=timeout) as resp:
        resp.raise_for_status()
        return await resp.json()

# Semaphore to cap concurrent requests
async def fetch_all(urls: list[str], limit: int = 10) -> list:
    semaphore = asyncio.Semaphore(limit)

    async def fetch_one(session, url):
        async with semaphore:
            return await fetch_json(session, url)

    async with aiohttp.ClientSession() as session:
        tasks = [fetch_one(session, url) for url in urls]
        return await asyncio.gather(*tasks, return_exceptions=True)

# Async generator for pagination
async def paginate(base_url: str):
    page = 1
    async with aiohttp.ClientSession() as session:
        while True:
            data = await fetch_json(session, f"{base_url}?page={page}")
            if not data:
                break
            for item in data:
                yield item
            page += 1

Tasks & Patterns

import asyncio

# Tasks run concurrently in the background
async def main():
    task1 = asyncio.create_task(fetch("api1.com"))
    task2 = asyncio.create_task(fetch("api2.com"))

    # Other work runs while tasks execute
    await asyncio.sleep(0)

    await asyncio.wait_for(task1, timeout=5.0)
    results = await asyncio.gather(task1, task2)

# Process results as they arrive
async def process_first_ready(urls: list[str]) -> None:
    tasks = [asyncio.create_task(fetch(url)) for url in urls]
    for completed in asyncio.as_completed(tasks):
        result = await completed
        print(f"Got: {result}")

# Queue-based producer/consumer
async def producer(queue: asyncio.Queue, items: list) -> None:
    for item in items:
        await queue.put(item)
    await queue.put(None)   # sentinel

async def consumer(queue: asyncio.Queue) -> None:
    while (item := await queue.get()) is not None:
        print(f"Processing: {item}")
        queue.task_done()

Standard Library Essentials

# pathlib — modern file system
from pathlib import Path
project = Path('/myproject')
config = project / 'config' / 'settings.json'
config.parent.mkdir(parents=True, exist_ok=True)
text = config.read_text(encoding='utf-8')
for f in project.rglob('*.py'):
    print(f.stem)

# datetime with timezone
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
tomorrow = now + timedelta(days=1)
iso = now.isoformat()

# collections
from collections import Counter, defaultdict, deque
freq = Counter("the quick brown fox the fox".split())
print(freq.most_common(2))   # [('the', 2), ('fox', 2)]

graph = defaultdict(list)
graph['A'].append('B')        # no KeyError on missing key

history = deque(maxlen=5)    # circular buffer
for i in range(10):
    history.append(i)
print(list(history))          # [5, 6, 7, 8, 9]

Popular Libraries

  • requests / httpx — HTTP clients; httpx adds async support and HTTP/2

  • pydantic v2 — data validation and settings management with type annotations

  • SQLAlchemy 2.0 — ORM and SQL toolkit, supports async with asyncpg

  • celery + redis/rabbitmq — distributed task queues and background jobs

  • pytest — testing framework with rich plugin ecosystem (pytest-asyncio, pytest-cov)

  • ruff — ultra-fast linter and formatter (replaces flake8, black, isort)

  • mypy / pyright — static type checkers

  • click / typer — CLI frameworks (typer leverages type annotations)

  • loguru — structured logging with minimal setup

Package Management

# Standard venv
python -m venv venv
source venv/bin/activate      # macOS/Linux
pip install fastapi uvicorn
pip freeze > requirements.txt

# uv — modern, Rust-based, very fast
pip install uv
uv venv && uv pip install fastapi uvicorn
uv pip sync requirements.txt  # exact reproducible installs

# Poetry — dependency management with lock files
poetry new myproject
poetry add fastapi sqlalchemy
poetry add --group dev pytest ruff mypy
poetry install && poetry run pytest
Python

Interview Questions

Python Interview Questions Covering language fundamentals, memory model, OOP, async, and best practices. 1. Explain the GIL. What are its implications? The Glob

Python Interview Questions

Covering language fundamentals, memory model, OOP, async, and best practices.

1. Explain the GIL. What are its implications?

The Global Interpreter Lock (CPython) prevents multiple native threads from executing Python bytecode simultaneously. For CPU-bound work, threads don't help — use multiprocessing (separate processes) or asyncio. For I/O-bound work (network, disk), threading works because the GIL is released during I/O waits. NumPy/SciPy release the GIL for their C-level computations, enabling real parallelism.

2. is vs ==

a = [1, 2, 3]; b = [1, 2, 3]; c = a
print(a == b)   # True  — same value (__eq__)
print(a is b)   # False — different objects
print(a is c)   # True  — same object

# CPython caches small ints (-5 to 256) and some strings
x = 256; y = 256; print(x is y)   # True (cached)
x = 257; y = 257; print(x is y)   # False (not cached)

3. The mutable default argument trap

def bad(item, lst=[]):    # list created ONCE at definition
    lst.append(item)
    return lst

bad(1)   # [1]
bad(2)   # [1, 2] — unexpected shared state!

def good(item, lst=None):   # correct pattern
    if lst is None:
        lst = []
    lst.append(item)
    return lst

4. How do decorators work?

import functools

def log(func):
    @functools.wraps(func)   # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Returned {result}")
        return result
    return wrapper

@log                   # equivalent to: add = log(add)
def add(a, b):
    return a + b

add(2, 3)  # Calling add → Returned 5

5. Generator vs list comprehension

# List: eager, O(n) memory, re-iterable
squares_list = [x**2 for x in range(1000)]

# Generator: lazy, O(1) memory, single-pass
squares_gen = (x**2 for x in range(1000))

# Use generators for large sequences or pipelines
total = sum(x**2 for x in range(1_000_000))  # efficient

6. Python memory management

  • CPython uses reference counting — each object tracks the number of references pointing to it

  • When reference count reaches 0, memory is freed immediately (deterministic destruction)

  • Cyclic garbage collector handles circular references, running periodically in generations

  • __slots__ reduces memory ~40-50% for classes with many instances by avoiding per-instance __dict__

7. asyncio vs threading

  • Threading: preemptive, OS-scheduled, one thread per OS thread — limited by GIL for CPU-bound

  • asyncio: cooperative, event-loop-scheduled, single-threaded — no GIL contention, zero thread overhead

  • Blocking code in asyncio freezes the event loop — use asyncio.run_in_executor() to offload to threads

  • asyncio shines at high-concurrency I/O (thousands of simultaneous requests with minimal memory)

8. @classmethod vs @staticmethod vs instance method

class User:
    def __init__(self, name: str):
        self.name = name

    def greet(self) -> str:           # instance method — has self
        return f"Hi, {self.name}"

    @classmethod
    def from_dict(cls, data: dict) -> 'User':   # factory / alternative constructor
        return cls(data['name'])

    @staticmethod
    def validate_name(name: str) -> bool:       # utility — no self/cls
        return len(name) >= 2

9. What are Python protocols?

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

class Circle:              # doesn't inherit from Drawable
    def draw(self) -> None:
        print("Drawing circle")

def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())   # works — structural typing (duck typing + type safety)

10. What is __slots__?

class Point:
    __slots__ = ('x', 'y')   # fixed attributes, no __dict__
    def __init__(self, x, y):
        self.x = x; self.y = y

# ~40-50% less memory per instance, faster attribute access
# Trade-off: cannot add arbitrary attributes dynamically
# Best for: millions of instances (data processing, simulations)

Keep your Python knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever