Skip to content

Python Deep Dive

263 companion flashcards · AI-assisted study content · Open the deck →

This deck is designed to take your Python knowledge past basic syntax and into the deeper concepts that experienced developers rely on every day. The cards cover the language's design philosophy, the differences between its various implementations, how Python's type system actually works, and the subtle distinctions between built-in data structures. Whether you're trying to articulate why == and is behave differently or remember what makes a tuple different from a list, these flashcards give you a precise vocabulary for talking about Python with confidence.

It's a great fit if you already write Python code regularly and want to strengthen your mental model of how the language works under the hood. You'll get the most out of it if you've spent at least a few months writing scripts, working through tutorials, or building small projects, and you're now looking to consolidate scattered knowledge into clear, well-defined concepts. It's also useful preparation for technical interviews, where questions about mutability, duck typing, and interpreter choices come up surprisingly often.

Because the cards focus on definitions and distinctions rather than step-by-step coding tasks, try answering each question out loud or in writing before flipping the card. This active recall practice will stick much better than passively reading the answers. A little review each day goes a long way, so consider working through a small batch of cards on a consistent schedule rather than trying to cram the whole deck in one sitting. When a concept feels abstract, pause and try to find a real example in code you've written, since connecting the terminology to something concrete helps lock it in.

Python Foundations and Principles

Python is a high-level, interpreted, dynamically typed language created by Guido van Rossum in 1991. The language is shaped by PEP 8, its official style guide, and PEP 20, Tim Peters' Zen of Python, which articulates guiding principles such as readability and explicitness. Python 3 is the actively maintained line; Python 2 reached end-of-life in 2020. The reference implementation is CPython, written in C. Alternative implementations include PyPy, which uses JIT compilation for speed, and Jython, which runs on the Java Virtual Machine.

Pythonic coding style favors clarity over cleverness. The principle "easier to ask forgiveness than permission" (EAFP) encourages trying an operation and catching exceptions rather than checking conditions up front, in contrast to "look before you leap" (LBYL). Values are evaluated as truthy or falsy in boolean contexts: empty containers, the integer 0, and None are falsy, while most other values are truthy. Boolean operators short-circuit, so `and` and `or` stop evaluating once the result is determined. Two related idioms follow: `x is None` checks specifically for None and is preferred over `== None`, while `not x` is true for any falsy value, not only None.

Comparison semantics distinguish identity from equality. The `==` operator checks value equality, while `is` checks identity — whether two names refer to the same object in memory. Similar syntactic distinctions appear throughout the language: the `match` statement uses pattern matching rather than equality with `==`, and the walrus operator `:=` assigns a value within an expression, enabling reuse of computed values without separate statements.

Core Data Types and Type Concepts

Python's dynamic type system relies on behavior-based dispatch, a property known as duck typing: rather than checking declared types, code asks whether an object can perform a required operation. PEP 544 codifies this idea through Protocol classes, which enable structural subtyping — a class qualifies by having the required methods, with no inheritance needed.

A central distinction is between mutable and immutable objects. Mutable types — lists, dictionaries, sets, and bytearrays — can be modified after creation, while immutable types like tuples, strings, integers, and bytes cannot. Lists and tuples differ in that lists are mutable, while tuples are immutable and therefore hashable, making them usable as dictionary keys or set elements. A dictionary is a mutable mapping of unique keys to values; a set is an unordered collection of unique, hashable elements; and a frozenset is an immutable, hashable variant of a set. `None` is Python's null value and the sole instance of `NoneType`.

For binary data, Python provides `bytes` — an immutable sequence — and `bytearray`, which is mutable. Strings are converted to bytes through encoding, typically UTF-8, a variable-width encoding that supersedes ASCII, the older 7-bit standard. A common pitfall involves mutable default arguments: defining `def f(x=[])` shares the same list across calls because the default is evaluated once at function-definition time. The idiomatic fix is to use `None` as a sentinel and construct a fresh list inside the function body.

Iteration, Generators, and Comprehensions

Python distinguishes iterables — objects that can return an iterator via `__iter__` — from iterators, which implement both `__iter__` and `__next__`. When an iterator is exhausted, it raises `StopIteration`. Comprehensions provide concise syntax for building collections: list comprehensions such as `[x*2 for x in range(10)]` produce lists, while dict and set comprehensions use braces. Generator expressions like `(x*2 for x in range(10))` look similar but evaluate lazily, producing one value at a time without materializing the entire sequence.

Generator functions extend laziness to ordinary functions, using `yield` to return a value while suspending execution state. The `yield from` syntax delegates iteration to another iterable or generator. Built-in iteration tools complete the picture: `range` produces sequences of integers, `enumerate` yields `(index, value)` pairs, and `zip` combines multiple iterables element-wise — stopping at the shortest, unlike `itertools.zip_longest`, which pads with a fill value. `map` applies a function to each item, `filter` retains items where a function returns truthy, and `any` and `all` reduce an iterable to a boolean. `sum`, `min`, and `max` provide aggregate reductions. A list comprehension is generally more readable than the equivalent `list(map(...))` call.

For ordering, `sorted()` returns a new sorted list, while `list.sort()` sorts in place. Both accept a `key` function that extracts the comparison value from each element, and a `reverse=True` flag for descending order. When modifying a list, `append` adds a single item, while `extend` concatenates an iterable. Choosing between eager comprehensions and lazy generators is especially valuable when working with large or potentially infinite sequences.

Functions, Decorators, and Modules

Functions are first-class objects that can be wrapped to modify behavior through decorators. The `@decorator` syntax above a definition is syntactic sugar equivalent to `f = decorator(f)`. `functools.wraps` preserves the original function's metadata — name, docstring, and other attributes — when decorating. Python's argument model supports flexible calling conventions: `*args` captures positional arguments as a tuple, `**kwargs` captures keyword arguments as a dictionary, and the same `*` and `**` operators unpack iterables and dicts when invoking a function. Parameters before `/` are positional-only, while parameters after `*` are keyword-only, controlling how callers may invoke the function.

Scope rules follow the LEGB order: Local, Enclosing, Global, and Built-in. The `global` keyword rebinds a name at module level, while `nonlocal` rebinds it in the nearest enclosing function scope. Closures capture variables from enclosing scopes, but reassigning them requires `nonlocal` — otherwise the assignment creates a new local variable instead of modifying the captured one. For copying compound objects, `copy.copy()` performs a shallow copy that duplicates the outer container but shares inner references, while `copy.deepcopy()` recursively duplicates everything. The `functools` module provides `lru_cache` for memoizing results by argument, `cache` (without a size limit), `partial` for pre-filling arguments, and `reduce` for cumulative application of a function.

Module organization follows several conventions. `import module` preserves the module's namespace, while `from module import name` brings specific names into scope. Relative imports use leading dots to navigate within a package. Circular imports — where modules import each other — often signal a design problem and can cause errors at import time. Python searches for modules in `sys.path`, which the `PYTHONPATH` environment variable can extend. The `__all__` attribute defines what names `from module import *` exports, documenting a module's public API. An `__init__.py` marks a directory as a package (namespace packages allow splitting across locations without one in Python 3.3+), and `__main__.py` designates the entry point for `python -m pkg`. Guarding top-level code with `if __name__ == "__main__":` ensures it runs only when the script is invoked directly.

Object-Oriented Programming and Dunder Methods

Python integrates objects with language syntax through dunder (double underscore) methods. `__init__` initializes a newly created instance, while `__new__` actually creates and returns it. `__repr__` returns an unambiguous string useful for debugging, while `__str__` returns a human-readable version. `__eq__` defines equality, and `__hash__` returns a hash value required for objects used as dictionary keys or set members. `__call__` makes instances callable like functions. Attribute access hooks include `__getattr__` (called when normal lookup fails) and `__getattribute__` (called for every access — powerful but easy to misuse). `__setattr__` runs when setting an attribute, `__getitem__` implements `obj[key]` indexing, `__iter__` returns an iterator, and `__enter__`/`__exit__` implement the context manager protocol used by the `with` statement for resource management. The `contextlib` module and `@contextmanager` decorator provide convenient ways to build context managers.

Class structure distinguishes class attributes (shared across all instances) from instance attributes (per-instance). `__slots__` restricts which instance attributes may be set, saving memory for high-volume classes. Multiple inheritance is supported, and Python resolves method lookup using the C3 linearization Method Resolution Order (MRO); `super()` returns a proxy for invoking parent methods. Abstract Base Classes cannot be instantiated directly and may declare methods marked with `@abstractmethod` that subclasses must implement. Structural subtyping via Protocol classes lets a class qualify by having required methods, in contrast to nominal subtyping, which checks declared inheritance. `isinstance()` checks whether an object is an instance of a class or its subclass, and `issubclass()` checks class-to-class relationships. `@classmethod` receives the class as `cls`, while `@staticmethod` receives neither class nor instance.

More advanced features include the descriptor protocol: objects implementing `__get__`, `__set__`, or `__delete__` control attribute access. `@property` makes a method accessible like an attribute, while `@cached_property` computes the value once and caches it. The `__set_name__` descriptor hook lets a descriptor learn the attribute name it's bound to. Metaclasses are classes whose instances are classes, controlling class creation; `type()` is the default metaclass and can also create classes dynamically. `__init_subclass__` runs when a subclass is created, enabling hooks. Dataclasses, declared with the `@dataclass` decorator, auto-generate `__init__`, `__repr__`, and equality methods from annotations. `dataclass(frozen=True)` makes instances immutable, `dataclass(slots=True)` generates `__slots__`, and `__post_init__` runs additional initialization after the generated `__init__`.

Type Hinting and Modern Language Features

PEP 484 introduced optional type annotations specifying expected types, supported by the `typing` module which provides generics like `List`, `Dict`, `Optional`, `Union`, and `Callable`. Static type checkers like mypy catch type errors before runtime. More advanced constructs include type aliases such as `UserID = int`, `TypeVar` for generic functions, `Generic` classes parameterized by type variables, `Optional[X]` (equivalent to `Union[X, None]`), `Literal` types (PEP 586) restricting values to specific literals, `TypedDict` (PEP 589) for dicts with fixed string keys and known value types, and `typing.Final` for names that cannot be reassigned.

String formatting has evolved through three eras: the oldest uses `%` formatting like `"%s %d" % (a, b)`, followed by `str.format()` with `"{} {}".format(a, b)`, and finally f-strings (PEP 498) such as `f"value={x}"`. F-strings also support a debug mode where `f"{x=}"` produces output like `x=value`. PEP 572 introduced the walrus operator `:=`, which assigns within an expression and is particularly useful inside comprehensions for reusing a computed value. Python 3.10 added structural pattern matching via `match`/`case` (PEP 634), where the `match` clause uses pattern syntax rather than equality comparison.

Recent Python versions keep expanding capabilities. Python 3.11 added exception groups for raising multiple unrelated exceptions together, task groups for structured concurrency in asyncio, and `tomllib` for reading TOML files. `asyncio.timeout()` raises a timeout error after a specified duration. PEP 684 in Python 3.12+ introduced per-interpreter GIL, enabling true parallelism between sub-interpreters. PEP 657 improved tracebacks to point at specific expressions rather than just lines, making debugging faster.

Concurrency: GIL, Threads, Processes, and Async

CPython's Global Interpreter Lock (GIL) ensures that only one thread executes Python bytecode at a time, simplifying memory management but limiting CPU-bound multithreading parallelism. For CPU-bound work the common remedies are multiprocessing or native extensions; threading remains well-suited to I/O-bound tasks, where the GIL is released during waits. The choice between threading and multiprocessing hinges on this trade-off: threads share memory and are constrained by the GIL, while processes have separate memory spaces and run truly in parallel.

The `asyncio` library provides single-threaded asynchronous concurrency through coroutines — functions defined with `async def` that can pause at `await` points. `await` yields control until the awaitable completes. The event loop is the runtime that schedules and runs coroutines; `asyncio.run()` serves as the top-level entry point. `asyncio.create_task()` schedules a coroutine as a Task, and `asyncio.gather()` runs multiple awaitables concurrently and collects their results. `asyncio.Queue` provides an async-safe queue for producer/consumer patterns. Thread safety and async safety are distinct concerns: locks and queues for threads address one scheduling model, while asyncio primitives address another.

Beyond asyncio, `concurrent.futures` offers a high-level API with `ThreadPoolExecutor` and `ProcessPoolExecutor`. On Unix systems, `os.fork()` creates a child process, though the `multiprocessing` module is generally preferred for portability. The `subprocess` module spawns and manages external processes. Together these tools let developers choose the right concurrency model — threads or coroutines for I/O-bound work, processes for CPU-bound parallelism.

Standard Library, Tooling, and Ecosystem

Python's standard library is extensive. The `collections` module adds specialized containers: `namedtuple` creates tuple subclasses with named fields, `OrderedDict` maintains insertion order (regular dicts also do in Python 3.7+), `defaultdict` provides defaults for missing keys, `Counter` tallies hashable objects, `deque` offers fast appends and pops at both ends, and `ChainMap` groups multiple dicts as one mapping. `itertools` provides iterator building blocks like `chain`, `cycle`, and `combinations`, while `functools` complements `operator` (functional versions of operators such as `operator.add`) with `lru_cache`, `partial`, and `reduce`. `heapq` implements priority queues and `bisect` provides binary search on sorted lists. Data interchange is handled by `json` (interoperable text), `pickle` (Python-specific binary — convenient but insecure with untrusted data), `csv` for tabular files, and `sqlite3` for embedded SQL. Networking lives in `urllib` (stdlib HTTP), `socket` (low-level), and the third-party `requests`. Numerical work spans `math` (functions and constants), `decimal` (arbitrary precision), `fractions` (rationals), `random` (pseudo-random), `secrets` (cryptographically secure), `hashlib` (cryptographic hashes), and `base64` (encoding). The `timeit` module benchmarks small snippets, complementing IPython's `%timeit` magic. Filesystem paths are modernized through `pathlib`, an object-oriented alternative to `os.path`.

For diagnostics and tooling, Python offers interactive environments and profilers. The REPL is the built-in interactive prompt; IPython enhances it with magics like `%timeit`, while Jupyter Notebook combines code, output, and narrative in a web app. Profiling measures performance to find bottlenecks: `cProfile` is the built-in profiler, `line_profiler` profiles line-by-line, and `memory_profiler` tracks memory line-by-line. PEP 657 tracebacks pinpoint specific expressions. The `logging` module provides levels (DEBUG, INFO, WARNING, ERROR, CRITICAL), handlers that direct records to files, consoles, or networks, formatters that specify record layout, and `logging.config.dictConfig` for dictionary-based setup. `logging.exception()` logs an ERROR with a traceback, whereas `print()` simply writes to stdout.

Packaging has evolved significantly. `venv` creates lightweight virtual environments; `virtualenv` preceded it. `pip` installs packages from PyPI, with `pip freeze` listing installed versions. `requirements.txt` lists dependencies, while `pyproject.toml` (PEP 518/621) is the modern configuration standard. Packages are distributed as wheels (.whl) for fast installation or sdists (source distributions). Tools like Poetry and Pipenv combine dependency management with virtualenvs, while conda is popular in data science. A package's `__version__` attribute conventionally holds version info, following semantic versioning (Major.Minor.Patch).

Exception handling centers on `try`/`except`. `BaseException` is the root, while `Exception` is the parent of most user-facing errors. The `else` block runs only on success, `finally` always runs, `raise` triggers an exception, and `raise from` chains a new exception to its cause. Custom exceptions inherit from `Exception` or a subclass. Testing tools include `unittest` (xUnit-style), `pytest` (with fixtures, monkeypatching, mocks, and `parametrize`), `doctest` (tests embedded in docstrings), coverage measurement via coverage.py, `tox` for testing across Python versions, and `hypothesis` for property-based testing. Unit tests isolate single components, integration tests combine them, and end-to-end tests exercise the full system. Test-driven development writes tests before implementation.

Web frameworks vary in scope: Django is full-featured with ORM and admin, Flask is a lightweight microframework, FastAPI is async, type-hinted, and auto-documented, and `aiohttp` is async HTTP. Deployment standards are WSGI (synchronous) and ASGI (asynchronous), served by `gunicorn` and `uvicorn` respectively. Documentation is generated with Sphinx using reStructuredText, and docstrings provide in-code documentation accessible via `help()`. Memory management combines reference counting — trackable via `sys.getrefcount` — with a cyclic garbage collector that handles circular references. The `__del__` destructor runs when an object is about to be destroyed, and `weakref` creates references that do not prevent garbage collection. Numeric helpers round out the picture: `divmod` returns quotient and remainder, `abs` returns absolute value, and `round` uses banker's rounding by default.

Frequently asked questions

What is Python?

A high-level, interpreted, dynamically typed programming language created by Guido van Rossum in 1991.

What is StopIteration?

Exception raised when an iterator is exhausted.

What is __hash__?

Returns hash value; required for use in sets/dicts as keys.

What is the LEGB rule?

Scope lookup order: Local, Enclosing, Global, Built-in.

What is setup.py?

Legacy build script for Python packages.

What is logging.exception()?

Logs an ERROR with exception traceback.

What is collections.defaultdict?

A dict with default values for missing keys.

What is FastAPI?

A modern web framework using type hints, async, and OpenAPI.

What is f-string debug mode?

f"{x=}" prints "x=value" (Python 3.8+).

What is the difference between zip and zip_longest?

zip stops at shortest; zip_longest pads with fillvalue.

Drill this topic

263 flashcards on Python Deep Dive — free, no signup needed to start.

Study Python Deep Dive flashcards

LearnWiki pages are generated with AI assistance from LearnCoachAssist's reviewed study catalog and may contain errors — verify anything critical against your course materials.