01. Basics - Python

Quote

“The only way to learn a new programming language is by writing programs in it.”

Brian W. Kernighan & Dennis Ritchie, The C Programming Language (1978)

Environment Setup

Covers Python interpreter configuration, virtual environment inspection, and package verification. These cells confirm the runtime environment before executing language examples.

Runtime and package inspection

Confirms the interpreter version, executable location, and installed packages before running language examples.

Check Python version, executable path, and hostname

Use sys.version, sys.executable, and socket.gethostname() to confirm which interpreter produced the evidence before trusting any downstream outputs.

This example prints the Python version, interpreter path, and host name.

import sys
import socket
from collections import deque, OrderedDict, defaultdict, Counter, namedtuple
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum, IntEnum
from fractions import Fraction
from importlib.metadata import distributions
from typing import NamedTuple
import io
import json
import math
import os
import time as _time
print(sys.version)   # Python version
print(sys.executable)   # Python executable
print(socket.gethostname())   # Machine
3.12.0 (tags/v3.12.0:0fb18b0, Oct  2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]
C:\Users\aperi\My Drive\VAULT\.vault\Scripts\python.exe
Elysium

Inspect virtual environment and working directory

os.environ.get('VIRTUAL_ENV') returns the path to the active virtual environment, or None if running in the system Python. os.getcwd() returns the current working directory where file path resolution starts.

This example prints the active virtual environment path and current working directory.

print(os.environ.get('VIRTUAL_ENV', 'None'))
print(os.getcwd())
None
C:\Users\aperi\My Drive\VAULT

List installed packages with importlib.metadata

importlib.metadata.distributions() iterates over all installed packages in the current environment. Each distribution object exposes metadata['Name'] and version. This replaces the older pkg_resources approach and works in virtual environments.

This example lists the first five installed distributions in the current environment.

installed = sorted([f"{d.metadata['Name']}=={d.version}" for d in distributions()])
for pkg in installed[:5]:
    print(pkg)
EbookLib==0.20
Jinja2==3.1.6
MarkupSafe==3.0.3
PyMuPDF==1.27.2.2
PyPika==0.51.1

Verify that required packages can be imported

__import__(name) dynamically imports a module by name string. Wrapping it in try/except catches ImportError for missing packages. This pattern is useful at the top of notebooks to fail fast with a clear message if a dependency is not installed.

This example imports common packages and reports whether each import succeeds.

imports = [
    "numpy", "pandas", "matplotlib",
    "sqlalchemy", "requests", "fastapi",
    "pydantic", "yaml", "pytest"
]
 
for mod in imports:
    try:
        __import__(mod)
        print(f"  {mod}: OK")
    except ImportError:
        print(f"  {mod}: MISSING")
  numpy: OK
  pandas: OK
  matplotlib: MISSING
  sqlalchemy: OK
  requests: OK
  fastapi: MISSING
  pydantic: OK
  yaml: OK
  pytest: MISSING

Console I/O

Demonstrates output formatting, escape sequences, terminal styling, and input parsing. Python’s print() is the primary output function, and input() reads text from stdin. All input arrives as str — numeric values must be parsed explicitly with int() or float() inside try/except.

Output and string formatting

Covers print() parameters (sep, end, file, flush) and the three string formatting styles: f-strings, .format(), and %-formatting.

print() accepts multiple arguments separated by sep (default " ") and ends with end (default "\n"). Accepts any type — calls str() on each argument automatically. Use sep instead of manual + concatenation. For structured logging, use the logging module instead.

This example prints multiple values with custom sep and end settings.

print("one", "two", "three", sep=" | ", end="\n")
one | two | three

Format strings with f-strings, .format(), and %-formatting

Python offers three string formatting approaches. f-strings (Python 3.6+) embed expressions directly in the string with {expr} — preferred for readability and performance. .format() uses numbered or named placeholders — useful when the format string is dynamic. %-formatting is the oldest style ("Name: %s") — still encountered in legacy code but not recommended for new projects. All three support format specifiers like :.2f for two decimal places.

This example formats the same values with f-strings, .format(), and legacy % formatting.

name = "Alice"
age = 30
 
# f-string
print(f"Name: {name}, Age: {age}")
 
# .format() method
print("Name: {}, Age: {}".format(name, age))
 
# % formatting (legacy)
print("Name: %s, Age: %d" % (name, age))
 
# f-string with expressions
print(age + 1)   # Next year
print(name.upper())   # Name uppercased
print(f"{3.14159:.2f}")   # Pi to 2 decimals
 
# Output formatting options for print()
Name: Alice, Age: 30
Name: Alice, Age: 30
Name: Alice, Age: 30
31
ALICE
3.14

Control separator between print() arguments with sep

The sep parameter controls what character(s) print() inserts between its arguments (default is a single space). This is Python’s equivalent of C#‘s string.Join — pass multiple values and let print() handle the joining. Works with any type that has a __str__ method.

This example changes the separator inserted between print() arguments.

print("a", "b", "c")                    # a b c
print("a", "b", "c", sep=", ")          # a, b, c
print("a", "b", "c", sep="")            # abc
print("a", "b", "c", sep="")         # a → b → c
print(2024, 3, 15, sep="-")             # 2024-3-15
a b c
a, b, c
abc
a → b → c
2024-3-15

Control line ending with end and force output with flush

The end parameter controls what print() appends after the output (default "\n"). Setting end="" or end=" " allows building a line incrementally across multiple print() calls. The file parameter redirects output to any file-like object (sys.stderr, an open file). The flush=True parameter forces immediate output, bypassing Python’s output buffering — essential for progress indicators in loops.

This example customizes print() line endings, writes to stderr, and forces an immediate flush.

print("hello", end=" ")
print("world", end="!\n")               # hello world!
print("loading", end="")
print("...", end="")
print("done!")                           # loading...done!
 
# flush — force immediate output (useful in loops/progress)
 
print("Processing...", flush=True)       # immediately visible, no buffering
print()
 
# file — redirect output to a file
 
print("This goes to stderr", file=sys.stderr)
# print("log entry", file=open("log.txt", "a"))  # append to file
hello world!
loading...done!
Processing...
 
This goes to stderr

Escape sequences and terminal styling

Demonstrates backslash escape sequences, raw string literals, ANSI terminal color codes, and the distinction between str() and repr().

Escape sequences and raw strings

Escape sequences insert special characters using a backslash prefix: \t (tab), \n (newline), \\ (literal backslash), \" and \' (quotes), \uXXXX (Unicode code point), and \0 (null character). Raw strings (r"...") disable escape processing — backslashes are treated as literal characters, ideal for regex patterns and Windows file paths. The output cell renders the null byte as the visible escape \0 so the markdown file remains text-safe and searchable.

This example prints tabs, newlines, backslashes, quotes, Unicode characters, and raw string literals.

print("Tab:\tafter tab")
print("Newline:\nafter newline")
print("Backslash: \\")
print("Quote: \"double\" and \'single\'")
print("Unicode: \u2764 \u2605 \u2602")  # ❤ ★ ☂
print(r"Null char: [\0] (invisible)")
print(r"Raw string: \n \t not escaped")
Tab:	after tab
Newline:
after newline
Backslash: \
Quote: "double" and 'single'
Unicode: ❤ ★ ☂
Null char: [\0] (invisible)
Raw string: \n \t not escaped

Apply ANSI color and style codes to terminal output

ANSI escape codes control text color and style in terminals. Python uses \033 (octal for ESC) instead of C#‘s \x1b. Each code is bracketed by \033[ and terminated with m. Always reset with \033[0m. Markdown preview does not interpret ANSI escape sequences, so the rendered preview below uses equivalent HTML styling instead of raw terminal control bytes.

This example applies ANSI escape codes for foreground color, background color, and bold styling.

print("\033[31mRed text\033[0m")
print("\033[32mGreen text\033[0m")
print("\033[1;34mBold blue text\033[0m")
print("\033[43m\033[30mBlack on yellow\033[0m")
Red text
Green text
Bold blue text
Black on yellow

Distinguish repr() from str() — developer vs user-friendly output

str() returns a human-readable representation (what print() displays). repr() returns a developer representation that, ideally, could recreate the object — it shows escape sequences, quote delimiters, and type information. In f-strings, !r applies repr() and !a applies ascii() (escapes non-ASCII characters). Use repr() for debugging and logging; use str() for user-facing output.

Compare str() with repr(), then use f-string !r and !a conversions to show escaped newlines and non-ASCII characters.

s = "hello\nworld"
print(str(s))
print(repr(s))
print(f"{s!r}")
print(f"{'café'!a}")
hello
world
'hello\nworld'
'hello\nworld'
'caf\xe9'

Console input and parsing

Shows how to read user input with input(), parse it to numeric types using int() and float(), and validate it with while True + try/except loops.

Read a line of text from standard input with input()

input(prompt) displays the prompt string and blocks until the user presses Enter, then returns the entire line as a str. All console input arrives as text — numeric values must be parsed explicitly with int(), float(), or Decimal(). Returns an empty string if the user presses Enter without typing.

This example reads a name from standard input and prints the returned string type.

name = input("Enter your name: ")
print(f"Hello, {name}!")
print(type(name))   # Type of input
Hello, Alex!
<class 'str'>

Parse string to integer safely with int() and try/except

int(string) converts a string to an integer, raising ValueError if the string is not a valid integer representation. Wrapping in try/except ValueError is the Pythonic equivalent of C#‘s int.TryParse — catch the exception rather than checking beforehand (EAFP: “Easier to Ask Forgiveness than Permission”).

This example parses user input with int() and handles invalid integers with try/except.

age_str = input("Enter your age: ")
 
# Unsafe conversion - crashes on invalid input
# age = int(age_str)
 
# Safe conversion with try/except
try:
    age = int(age_str)
    print(f"Your age is {age}, type: {type(age)}")
except ValueError:
    print(f"'{age_str}' is not a valid integer")
Your age is 47, type: <class 'int'>

Parse string to float safely with float() and try/except

float(string) works identically to int() but for floating-point values. It respects locale-independent format (always uses . as the decimal separator). For locale-aware parsing, use the locale module. For financial amounts, parse to Decimal instead of float to avoid binary rounding.

This example parses user input with float() and handles invalid numbers with try/except.

price_str = input("Enter a price: ")
 
try:
    price = float(price_str)
    print(f"Price: ${price:.2f}, type: {type(price)}")
except ValueError:
    print(f"'{price_str}' is not a valid number")
Price: $30.00, type: <class 'float'>

Validate input in a loop until parsing succeeds

The standard pattern for interactive console input: while True + try/except + return. The loop repeats until valid input is provided. This is more Pythonic than pre-checking with regex — let the parser do the validation and catch the exception.

This example defines a reusable loop that keeps prompting until int() succeeds.

def get_valid_int(prompt):
    while True:
        value = input(prompt)
        try:
            return int(value)
        except ValueError:
            print(f"'{value}' is not valid. Please enter a whole number.")

Float input validation with the same while/try pattern

Same while True + try/except ValueError pattern applied to float() conversion. The structure is identical — only the conversion function changes.

This example defines the same reusable validation pattern for float() input.

def get_valid_float(prompt):
    while True:
        value = input(prompt)
        try:
            return float(value)
        except ValueError:
            print(f"'{value}' is not valid. Please enter a number.")

Reject empty or whitespace-only input with strip() and truthiness

Python’s truthy/falsy semantics make string validation concise: "".strip() returns "" which is falsy, so if value: rejects empty and whitespace-only strings. No explicit length check needed.

This example strips input and rejects empty or whitespace-only strings.

def get_non_empty_string(prompt):
    while True:
        value = input(prompt).strip()
        if value:
            return value
        print("Input cannot be empty.")

Call the reusable validation functions

Demonstrates calling the validators defined above. In a real console application, input() would prompt the user interactively; in a notebook, the values are pre-supplied.

This example calls the reusable validators and prints the accepted values.

num = get_valid_int("Enter a number: ")
print(num)   # Got
 
name = get_non_empty_string("Enter your name: ")
print(name)   # Got
17
Alex

Variables, Constants & Data Types

Covers variable declaration, naming conventions for constants, the complete set of built-in types, and Python’s mutable/immutable distinction. Unlike C#, Python is dynamically typed — variables can be rebound to any type at any time.

Variable declaration and constants

Explains Python’s dynamic typing model, where variables are names bound to objects at runtime, and the ALL_CAPS naming convention used for values that should not change.

Declare variables with dynamic typing — type() and isinstance()

Assign directly: x = 10 — Python infers the type at runtime. Variables are names bound to objects; rebinding changes the reference. type() reveals the current type; isinstance() checks the type hierarchy. Add type hints (PEP 484) for public APIs and complex functions.

This example binds names to different runtime types and then rebinds one variable to a string.

x = 10          # int
y = 3.14        # float
name = "Alice"  # str
active = True   # bool
 
# Python infers the type from the assigned value
print(f"x = {x}, type: {type(x)}")
print(f"y = {y}, type: {type(y)}")
print(f"name = {name}, type: {type(name)}")
print(f"active = {active}, type: {type(active)}")
 
# Variables can change type (dynamic typing)
x = "now I'm a string"
print(f"\nx = {x}, type: {type(x)}")
x = 10, type: <class 'int'>
y = 3.14, type: <class 'float'>
name = Alice, type: <class 'str'>
active = True, type: <class 'bool'>
 
x = now I'm a string, type: <class 'str'>

Define constants with UPPERCASE naming convention

Python has no const keyword — constants are a naming convention only. Use ALL_CAPS names for values that should not change. Nothing prevents reassignment at runtime, but linters like mypy can flag mutations of Final annotated variables (PEP 591, Python 3.8+): PI: Final = 3.14159.

This example declares conventional constants and shows that Python still allows reassignment at runtime.

PI = 3.14159
MAX_USERS = 100
API_URL = "https://api.example.com"
 
print(f"PI = {PI}")
print(f"MAX_USERS = {MAX_USERS}")
print(f"API_URL = {API_URL}")
 
# Nothing prevents reassignment (it's just a convention)
PI = 999  # Reassignment is allowed at runtime
PI = 3.14159
MAX_USERS = 100
API_URL = https://api.example.com

Numeric types

Covers Python’s four numeric types — int (arbitrary precision), float (64-bit IEEE 754), complex (built-in j notation), and Decimal (exact base-10 arithmetic for financial work).

Integer — arbitrary precision with no overflow

Python integers have arbitrary precision — they grow as large as memory allows, with no fixed bit width and no overflow. 10**100 works natively without any special library. This is fundamentally different from C#‘s fixed-width int (32-bit) and long (64-bit): Python allocates additional memory instead of overflowing or wrapping. The tradeoff is that very large integers are slower than fixed-width machine integers and consume more memory.

This example prints small and very large integers, their digit count, and their memory size.

a = 42
b = -100
c = 10**100
 
print(f"a = {a}")
print(f"b = {b}")
print(f"c = {c}")
print(f"c has {len(str(c))} digits")
print(f"Size of a in bytes: {sys.getsizeof(a)}")
print(f"Size of c in bytes: {sys.getsizeof(c)}")
print()
print("Python integers grow as needed")
a = 42
b = -100
c = 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
c has 101 digits
Size of a in bytes: 28
Size of c in bytes: 72
 
Python integers grow as needed

float — IEEE 754 double-precision (64-bit)

Python’s float is always 64-bit double-precision IEEE 754 — there is no separate float/double/decimal trio like C#. For exact decimal arithmetic, use Decimal from the decimal module. sys.float_info exposes the platform’s float characteristics including max value, min positive value, and digit precision.

This example prints representative float values and the platform limits reported by sys.float_info.

a = 3.14
b = -0.001
c = 1.8e308   # near max
d = 5e-324    # near min positive
 
print(f"a = {a}")
print(f"b = {b}")
print(f"c = {c}")
print(f"d = {d}")
print(sys.float_info.max)   # Max float
print(sys.float_info.min)   # Min float
print(f"Precision: {sys.float_info.dig} digits")
a = 3.14
b = -0.001
c = inf
d = 5e-324
1.7976931348623157e+308
2.2250738585072014e-308
Precision: 15 digits

Inspect special float values — Infinity, NaN, and precision loss

IEEE 754 defines three special float values: inf (positive infinity), -inf, and nan (Not a Number). Create them with float('inf'), float('nan'), or via operations like 1.0 / 0.0. nan is not equal to anything, including itself — use math.isnan() to test. The classic 0.1 + 0.2 != 0.3 artifact is inherent to binary floating-point; use Decimal or math.isclose() for exact comparison.

This example prints positive infinity, negative infinity, NaN, and the 0.1 + 0.2 precision artifact.

print(float('inf'))   # Infinity
print(float('-inf'))   # Neg Infinity
print(float('nan'))   # NaN
print()
 
# Floating point imprecision
print(0.1 + 0.2)  # IEEE 754 rounding artifact
inf
-inf
nan
 
0.30000000000000004

Perform complex number arithmetic with built-in j notation

Python has a built-in complex type using j suffix for the imaginary part (not i as in mathematics). Access components with .real and .imag. abs(z) returns the magnitude, .conjugate() returns the complex conjugate. Use cmath module for complex-valued math functions.

This example prints a complex number, its components, its conjugate, and its magnitude.

z = 3 + 4j
print(f"z = {z}, type: {type(z)}")
print(f"Real: {z.real}, Imaginary: {z.imag}")
print(z.conjugate())   # Conjugate
print(abs(z))  # magnitude
z = (3+4j), type: <class 'complex'>
Real: 3.0, Imaginary: 4.0
(3-4j)
5.0

Boolean, bytes, and None

Covers the bool type and its integer inheritance, the bytes/bytearray pair for binary data, and None as Python’s null singleton.

bool — truthy/falsy values and int subclass behavior

bool is a subclass of int with True == 1 and False == 0 — arithmetic with booleans works (True + True == 2). Unlike C#, Python evaluates any object in a boolean context: 0, 0.0, "", [], {}, set(), None, and range(0) are all falsy; everything else is truthy. Custom classes can define __bool__ to control their truthiness.

This example shows boolean arithmetic and the truthiness of common values.

a = True
b = False
 
print(f"a = {a}, type: {type(a)}")
print(f"b = {b}, type: {type(b)}")
print(True + True)  # 2 (bool is int)
print(True * 10)  # 10
 
# Truthy and falsy values
print()
print(bool(0))  # False
print(bool(1))  # True
print(bool(''))  # False
print(bool('hi'))  # True
print(bool([]))  # False
print(bool([1]))  # True
print(bool(None))  # False
a = True, type: <class 'bool'>
b = False, type: <class 'bool'>
2
10
 
False
True
False
True
False
True
False

Convert between strings and byte arrays with encode/decode

bytes is an immutable sequence of integers (0–255), created with b"..." literals or .encode(). bytearray is the mutable counterpart. str.encode("utf-8") converts a string to bytes; bytes.decode("utf-8") converts back. UTF-8 is variable-width: "café" encodes to 5 bytes because é requires two bytes (\xc3\xa9).

This example converts text to UTF-8 bytes and decodes the bytes back to a string.

b1 = b"hello"              # bytes (immutable)
b2 = bytearray(b"hello")   # bytearray (mutable)
 
print(f"b1 = {b1}, type: {type(b1)}")
print(f"b2 = {b2}, type: {type(b2)}")
 
# Encoding/decoding
text = "café"
encoded = text.encode("utf-8")
decoded = encoded.decode("utf-8")
print(f"\n'{text}' encoded: {encoded}")
print(f"decoded back: {decoded}")
b1 = b'hello', type: <class 'bytes'>
b2 = bytearray(b'hello'), type: <class 'bytearray'>
 
'café' encoded: b'caf\xc3\xa9'
decoded back: café

Understand None — Python’s null singleton

None is Python’s null value — a singleton of type NoneType. Always test with is None (identity check), not == None (equality check), because == can be overridden by custom classes. Functions without an explicit return statement return None. Use Optional[T] type hint (or T | None in Python 3.10+) to annotate variables that may be None.

This example shows the runtime type of None and compares is None with == None.

x = None
print(f"x = {x}, type: {type(x)}")
print(x is None)  # preferred way to check
print(x == None)  # works but not idiomatic
x = None, type: <class 'NoneType'>
True
True

Type system reference — all built-in types

The diagram below maps every built-in type to the common object root; the H4 cells that follow show each category with runtime examples.

This diagram maps Python’s built-in types from the shared object root before the category-specific runtime examples.


flowchart TD
    O["object"] --> INT["int (bool)"]
    O --> FLT["float"]
    O --> CPX["complex"]
    O --> STR["str"]
    O --> BYT["bytes / bytearray"]
    O --> LST["list"]
    O --> TUP["tuple (namedtuple)"]
    O --> DCT["dict (defaultdict, Counter, OrderedDict)"]
    O --> SET["set / frozenset"]
    O --> RNG["range"]
    O --> NON["NoneType"]
    O --> FN["function"]
    O --> DEC["Decimal / Fraction"]

Numeric types — int, float, complex, bool

Complete reference showing all numeric types with their runtime type names. Python has four numeric types: int (arbitrary precision), float (64-bit IEEE 754), complex (real + imaginary), and bool (subclass of int, True=1/False=0).

This example prints representative values for Python’s numeric types and their runtime type names.

i = 42
print(f"int:          {i:>20}  type={type(i).__name__}")
 
# float — 64-bit IEEE 754
f = 3.14
print(f"float:        {f:>20}  type={type(f).__name__}")
 
# complex — real + imaginary
c = 3 + 4j
print(f"complex:      {str(c):>20}  type={type(c).__name__}")
 
# bool — True/False (subclass of int)
b = True
print(f"bool:         {b:>20}  type={type(b).__name__}")
 
# str — immutable Unicode string
s = "hello"
print(f"str:          {s:>20}  type={type(s).__name__}")
int:                            42  type=int
float:                        3.14  type=float
complex:                    (3+4j)  type=complex
bool:                            1  type=bool
str:                         hello  type=str

Immutable sequence types — bytes, tuple, frozenset, range, None

Immutable types cannot be modified after creation. They are safe as dict keys, function defaults, and shared state. tuple is the immutable counterpart of list; frozenset is the immutable counterpart of set; range produces integers lazily without allocating a list.

This example prints common immutable built-in types and their runtime type names.

by = b"hello"
print(f"bytes:        {str(by):>20}  type={type(by).__name__}")
 
# tuple — immutable ordered sequence
t = (1, 2, 3)
print(f"tuple:        {str(t):>20}  type={type(t).__name__}")
 
# frozenset — immutable set
fs = frozenset({1, 2, 3})
print(f"frozenset:    {str(fs):>20}  type={type(fs).__name__}")
 
# range — immutable sequence of numbers
r = range(5)
print(f"range:        {str(r):>20}  type={type(r).__name__}")
 
# NoneType — singleton null
n = None
print(f"NoneType:     {str(n):>20}  type={type(n).__name__}")
bytes:                    b'hello'  type=bytes
tuple:                   (1, 2, 3)  type=tuple
frozenset:    frozenset({1, 2, 3})  type=frozenset
range:                 range(0, 5)  type=range
NoneType:                     None  type=NoneType

Mutable collection types — list, dict, set, bytearray

Mutable types can be modified in place. Assignment copies the reference (not the data), so two variables can point to the same object. list is a dynamic array, dict is a hash map, set stores unique elements, and bytearray is a mutable byte buffer.

This example prints common mutable built-in types and their runtime type names.

lst = [1, 2, 3]
print(f"list:         {str(lst):>20}  type={type(lst).__name__}")
 
# dict — key-value mapping
d = {"a": 1, "b": 2}
print(f"dict:         {str(d):>20}  type={type(d).__name__}")
 
# set — unordered unique elements
st = {1, 2, 3}
print(f"set:          {str(st):>20}  type={type(st).__name__}")
 
# bytearray — mutable byte sequence
ba = bytearray(b"hello")
print(f"bytearray:    {str(ba):>20}  type={type(ba).__name__}")
list:                    [1, 2, 3]  type=list
dict:             {'a': 1, 'b': 2}  type=dict
set:                     {1, 2, 3}  type=set
bytearray:     bytearray(b'hello')  type=bytearray

Specialized containers — memoryview, deque, OrderedDict

memoryview provides zero-copy access to the buffer of a bytes-like object — essential for high-performance I/O and image processing. deque (double-ended queue) supports O(1) append/pop on both ends. OrderedDict explicitly preserves insertion order (redundant since Python 3.7 where all dicts are ordered, but useful when order-dependent equality matters).

This example prints stable summaries for memoryview, deque, and OrderedDict.

mv = memoryview(ba)
print(f"memoryview:   itemsize={mv.itemsize}, nbytes={mv.nbytes}, readonly={mv.readonly}  type={type(mv).__name__}")
 
 
# deque — double-ended queue
dq = deque([1, 2, 3])
print(f"deque:        {dq}  (fast append/pop both ends)")
 
# OrderedDict — insertion-ordered dict (redundant since 3.7, dict keeps order)
od = OrderedDict(a=1, b=2)
print(f"OrderedDict:  {od}  (explicit ordered dict)")
memoryview:   itemsize=1, nbytes=5, readonly=False  type=memoryview
deque:        deque([1, 2, 3])  (fast append/pop both ends)
OrderedDict:  OrderedDict({'a': 1, 'b': 2})  (explicit ordered dict)

Auto-create missing keys with defaultdict

defaultdict(factory) automatically creates a default value when a missing key is accessed, eliminating the need for if key not in dict checks. Pass int for counting (defaults to 0), list for grouping (defaults to []), or set for unique grouping. This is Python’s most idiomatic pattern for accumulation and grouping tasks.

This example accumulates counts and groups values with defaultdict.

dd = defaultdict(int)          # factory=int → missing keys default to 0
dd["a"] += 1
dd["b"] += 3
dd["a"] += 2
print(dict(dd))
 
# defaultdict(list) — group items by key without checking if key exists
groups = defaultdict(list)
for name, dept in [("Alice", "Eng"), ("Bob", "Sales"), ("Charlie", "Eng")]:
    groups[dept].append(name)
print(dict(groups))
{'a': 3, 'b': 3}
{'Eng': ['Alice', 'Charlie'], 'Sales': ['Bob']}

Count occurrences of hashable elements with Counter

Counter is a dict subclass that maps elements to their counts. Pass any iterable — a string (counts characters), a list (counts elements), or use .update() to add more counts. .most_common(n) returns the n most frequent elements as (element, count) tuples. Supports arithmetic: counter1 + counter2 adds counts, counter1 - counter2 subtracts.

This example counts characters, prints the most common entries, and counts words.

ct = Counter("abracadabra")
print(ct)   # Counter
print(f"Most common 3: {ct.most_common(3)}")
 
# Counting words
words = "the cat sat on the mat the cat".split()
word_counts = Counter(words)
print(word_counts)
Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
Most common 3: [('a', 5), ('b', 2), ('r', 2)]
Counter({'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1})

Create lightweight immutable records with namedtuple

namedtuple creates a tuple subclass with named fields — combining the immutability and hashability of tuples with the readability of attribute access (pt.x instead of pt[0]). _replace() returns a new instance with one or more fields changed (immutable update). For more features (defaults, methods, type hints), use @dataclass(frozen=True) or typing.NamedTuple.

This example creates a namedtuple, reads fields by name and index, and returns an updated copy with _replace().

Point = namedtuple("Point", ["x", "y"])
pt = Point(3, 4)
print(pt)   # Point
print(f"pt.x:    {pt.x}, pt.y: {pt.y}")
print(pt[0])  # also indexable like a regular tuple
 
# _replace returns a NEW namedtuple with one field changed (immutable)
pt2 = pt._replace(x=10)
print(f"_replace: {pt2}")
Point(x=3, y=4)
pt.x:    3, pt.y: 4
3
_replace: Point(x=10, y=4)

Perform exact decimal arithmetic with Decimal

Decimal from the decimal module provides exact base-10 arithmetic — Python’s equivalent of C#‘s decimal. Use it for financial calculations, tax computation, and any domain where binary floating-point rounding (0.1 + 0.2 != 0.3) is unacceptable. Always construct from strings (Decimal("0.1")) — constructing from floats (Decimal(0.1)) captures the float’s imprecision.

This example compares exact Decimal arithmetic with floating-point arithmetic and computes a rounded total.

from decimal import Decimal, getcontext
 
getcontext().prec = 28
 
a = Decimal("0.1") + Decimal("0.2")
print(f"Decimal: 0.1 + 0.2 = {a}")
print(f"float:   0.1 + 0.2 = {0.1 + 0.2}")
 
price = Decimal("19.99")
tax = Decimal("0.0825")
total = (price * (1 + tax)).quantize(Decimal("0.01"))
print(f"Price: {price}, Tax: {tax}, Total: {total}")
Decimal: 0.1 + 0.2 = 0.3
float:   0.1 + 0.2 = 0.30000000000000004
Price: 19.99, Tax: 0.0825, Total: 21.64

Mutability and references

Demonstrates how immutable types create new objects on “reassignment” while mutable types share the same object across multiple references.

Exact rational arithmetic with Fraction and immutable string behavior

Fraction represents exact rational numbers with no floating-point error — Fraction(1, 3) is precisely one-third, not 0.33333.... Strings demonstrate immutability: += creates a new string object rather than modifying in place, so the original reference (b) remains unchanged.

This example prints an exact rational Fraction and shows that string reassignment creates a new object.

frac = Fraction(1, 3)
print(f"{frac}  (exact rational)")
 
# Immutable: reassignment creates a NEW object
a = "hello"
b = a
a += " world"
print(f"a='{a}', b='{b}'")
1/3  (exact rational)
a='hello world', b='hello'

Mutable list assignment shares the same object

Assigning a list to another variable copies the reference, not the data. Both variables point to the same list object — append() through either variable is visible through the other. Use b = a.copy() or b = a[:] for a shallow copy, or copy.deepcopy(a) for a deep copy of nested structures.

This example appends through one list reference and shows that both names still point to the same object.

a = [1, 2, 3]
b = a
a.append(4)
print(f"a={a}, b={b}")
print(f"Same object?   {a is b}")
a=[1, 2, 3, 4], b=[1, 2, 3, 4]
Same object?   True

Operators

Covers arithmetic, comparison, logical, bitwise, assignment, and identity/membership operators. Python has true division (/), floor division (//), and exponentiation (**) built in — no Math.Pow needed. Chained comparisons (a < b < c) and walrus operator (:=) are Python-specific features.

Arithmetic and comparison

Covers Python’s full set of arithmetic operators — including floor division and exponentiation — and the comparison operators that return bool.

Arithmetic operators — true division, floor division, and exponentiation

Python has true division (/ returns float), floor division (// returns int), and exponentiation (**). divmod(a, b) returns (quotient, remainder) in one call. For financial math, use Decimal for exact division.

This example prints arithmetic results and highlights Python’s true-division and floor-division behavior.

a, b = 17, 5
 
print(f"{a} + {b}  = {a + b}")  # Addition
print(f"{a} - {b}  = {a - b}")  # Subtraction
print(f"{a} * {b}  = {a * b}")  # Multiplication
print(f"{a} / {b}  = {a / b}")  # Division (always returns float)
print(f"{a} // {b} = {a // b}")  # Floor division (integer result)
print(f"{a} % {b}  = {a % b}")  # Modulus (remainder)
print(f"{a} ** {b} = {a ** b}")  # Exponentiation
print(f"-{a}       = {-a}")  # Unary negation
 
# Division behavior
 
print(f"7 / 2   = {7 / 2}")
print(f"7 // 2  = {7 // 2}")
print(f"-7 // 2 = {-7 // 2}")
print(f"7 % 2   = {7 % 2}")
print(f"-7 % 2  = {-7 % 2}")
17 + 5  = 22
17 - 5  = 12
17 * 5  = 85
17 / 5  = 3.4
17 // 5 = 3
17 % 5  = 2
17 ** 5 = 1419857
-17       = -17
7 / 2   = 3.5
7 // 2  = 3
-7 // 2 = -4
7 % 2   = 1
-7 % 2  = 1

Comparison operators — equality, inequality, and relational

Comparison operators return bool. Python compares by value for all built-in types (no reference vs value distinction like C#). == tests equality, is tests identity (same object in memory).

This example prints the standard equality and relational comparison results.

a, b = 10, 20
 
print(f"{a} == {b}  : {a == b}")  # Equal
print(f"{a} != {b}  : {a != b}")  # Not equal
print(f"{a} > {b}   : {a > b}")  # Greater than
print(f"{a} < {b}   : {a < b}")  # Less than
print(f"{a} >= {b}  : {a >= b}")  # Greater than or equal
print(f"{a} <= {b}  : {a <= b}")  # Less than or equal
10 == 20  : False
10 != 20  : True
10 > 20   : False
10 < 20   : True
10 >= 20  : False
10 <= 20  : True

Chain comparisons for concise range checks

Python supports chained comparisons: 10 < x < 20 is equivalent to (10 < x) and (x < 20) but evaluates x only once. This is more readable than C#‘s required 10 < x && x < 20. Chains can mix operators: 1 < 2 > 0 means 1 < 2 and 2 > 0.

This example prints chained comparisons for range checks and mixed operators.

x = 15
 
print(f"10 < {x} < 20 : {10 < x < 20}")  # True — same as (10 < x) and (x < 20)
print(f"1 < 2 < 3 < 4 : {1 < 2 < 3 < 4}")
print(f"1 < 2 > 0     : {1 < 2 > 0}")
10 < 15 < 20 : True
1 < 2 < 3 < 4 : True
1 < 2 > 0     : True

Identity, membership, and logical operators

Covers is/is not for object identity, in/not in for membership testing, and and/or/not with their short-circuit and operand-returning semantics.

Test object identity with is and is not

is checks whether two variables refer to the same object in memory (like C#‘s object.ReferenceEquals). == checks value equality. For singletons like None, always use is: if x is None. For small integers (-5 to 256) and interned strings, is may return True unexpectedly due to Python’s caching — never rely on is for value comparison.

This example prints value equality and object identity for separate and shared lists.

a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(f"a == b  : {a == b}")
print(f"a is b  : {a is b}")
print(f"a is c  : {a is c}")
print(f"a is not b : {a is not b}")
a == b  : True
a is b  : False
a is c  : True
a is not b : True

Test membership with in and not in

in tests whether a value exists in a collection — works on lists, tuples, sets, dicts (tests keys), strings (tests substrings), and ranges. Time complexity is O(1) for sets and dicts, O(n) for lists and tuples. C# equivalent is .Contains() or LINQ .Any().

This example prints membership tests for lists, strings, ranges, dictionaries, and boolean operators.

fruits = ["apple", "banana", "cherry"]
print(f"'banana' in fruits     : {'banana' in fruits}")
print(f"'grape' not in fruits  : {'grape' not in fruits}")
print(f"'an' in 'banana'       : {'an' in 'banana'}")
print(f"3 in range(5)          : {3 in range(5)}")
print(f"'key' in {{'key': 1}}    : {'key' in {'key': 1}}")  # works on dict keys
 
print(f"True and False  : {True and False}")
print(f"True or False   : {True or False}")
print(f"not True        : {not True}")
'banana' in fruits     : True
'grape' not in fruits  : True
'an' in 'banana'       : True
3 in range(5)          : True
'key' in {'key': 1}    : True
True and False  : False
True or False   : True
not True        : False

Logical operators return operands, not just True/False

Unlike C#‘s &&/|| which always return bool, Python’s and/or return the actual operand that determined the result. and returns the first falsy value (or the last value if all truthy). or returns the first truthy value (or the last value if all falsy). This enables the None or 'default' pattern for providing fallback values — Python’s equivalent of C#‘s ?? null-coalescing.

This example prints the raw operand returned by each and and or expression.

print(f"0 and 5         : {0 and 5}")
print(f"3 and 5         : {3 and 5}")
print(f"'' and 'hello'  : {'' and 'hello'}")
 
# 'or' returns first truthy value or last value
print(f"0 or 5          : {0 or 5}")
print(f"3 or 5          : {3 or 5}")
print(f"'' or 'hello'   : {'' or 'hello'}")
print(f"None or 'default': {None or 'default'}")
0 and 5         : 0
3 and 5         : 5
'' and 'hello'  :
0 or 5          : 5
3 or 5          : 3
'' or 'hello'   : hello
None or 'default': default

Truthy/Falsy values0, 0.0, "", [], {}, set(), None, and range(0) are all falsy; every other value is truthy. Custom classes can define __bool__ to control their truthiness.

This example prints the boolean value of common falsy objects.

falsy_values = [False, 0, 0.0, 0j, "", [], {}, set(), None, range(0)]
for val in falsy_values:
    print(f"  bool({str(val):10}) = {bool(val)}")
  bool(False     ) = False
  bool(0         ) = False
  bool(0.0       ) = False
  bool(0j        ) = False
  bool(          ) = False
  bool([]        ) = False
  bool({}        ) = False
  bool(set()     ) = False
  bool(None      ) = False
  bool(range(0, 0)) = False

Bitwise operators and flags

Covers Python’s bitwise operators (&, |, ^, ~, <<, >>) and the practical flag-management pattern built on top of them.

Bitwise AND, OR, XOR, NOT, and shift operators

Bitwise operators work on the binary representation of integers. Python integers have arbitrary precision, so ~a inverts all bits (including the sign bit via two’s complement), producing -(a+1). The operators are identical to C#: & (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), >> (right shift). Python has no unsigned right shift (>>>) since integers are arbitrary-width.

This example prints the result of each bitwise operator in binary and decimal form.

a, b = 0b1100, 0b1010  # 12 and 10
 
print(f"a = {a:04b} ({a}),  b = {b:04b} ({b})")
print(f"a & b  (AND)  = {a & b:04b} ({a & b})")  # 1000 (8)
print(f"a | b  (OR)   = {a | b:04b} ({a | b})")  # 1110 (14)
print(f"a ^ b  (XOR)  = {a ^ b:04b} ({a ^ b})")  # 0110 (6)
print(f"~a     (NOT)  = {~a} (inverts all bits)")  # -13 (two's complement)
print(f"a << 2 (LEFT) = {a << 2:08b} ({a << 2})")  # 110000 (48)
print(f"a >> 1 (RIGHT)= {a >> 1:04b} ({a >> 1})")  # 0110 (6)
 
# Common use cases for bitwise operators
a = 1100 (12),  b = 1010 (10)
a & b  (AND)  = 1000 (8)
a | b  (OR)   = 1110 (14)
a ^ b  (XOR)  = 0110 (6)
~a     (NOT)  = -13 (inverts all bits)
a << 2 (LEFT) = 00110000 (48)
a >> 1 (RIGHT)= 0110 (6)

Manage permission flags with bitwise operations

Same flag pattern as C#: define each permission as a power of 2, combine with |, test with &, add with |=, remove with &= ~flag. Python has no [Flags] enum equivalent, but enum.IntFlag (Python 3.6+) provides similar functionality with named flags and readable repr().

This example combines flags, tests individual permissions, and removes a flag with bitwise masks.

READ, WRITE, EXECUTE = 0b100, 0b010, 0b001
 
# Combine two flags with OR
perms = READ | WRITE
print(f"{perms:03b}")   # Start
 
# Test a flag with AND
print(f"Can read?        {bool(perms & READ)}")
print(f"Can execute?     {bool(perms & EXECUTE)}")
 
# Add a flag — set its bit with |=
perms |= EXECUTE
print(f"{perms:03b}")   # After |= EXEC
 
# Remove a flag — AND with inverted mask using &= ~
perms &= ~WRITE
print(f"After &= ~WRITE: {perms:03b}")
 
# Even/odd check — LSB is 1 for odd, 0 for even
n = 42
print(f"\n{n} is {'even' if n & 1 == 0 else 'odd'}")
110
Can read?        True
Can execute?     False
111
After &= ~WRITE: 101
 
42 is even

Swap values with XOR or tuple unpacking

XOR swap works the same as in C#, but Python’s idiomatic swap is x, y = y, x (tuple unpacking) — clearer, faster, and works for any type. The XOR trick is shown for completeness but should never be used in Python code.

XOR swap works because XOR is its own inverse: a ^ b ^ b == a. The algorithm proceeds in three steps: (1) x = x^yx now holds the combined bits; (2) y = (x^y)^y — recovers the original x; (3) x = (x^y)^x — recovers the original y. Always prefer x, y = y, x in real code — this is a curiosity, not a recommendation.

This example swaps two integers with the XOR trick and prints the final values.

x, y = 5, 10
x ^= y; y ^= x; x ^= y
print(f"Swapped: x={x}, y={y}")
Swapped: x=10, y=5

Assignment and compound operators

Documents Python’s compound assignment operators for arithmetic and bitwise operations, including the Python-specific //= and **= forms.

Compound assignment operators — arithmetic shorthand

Same compound operators as C# (+=, -=, *=, /=, %=) plus Python-specific //= (floor division) and **= (exponentiation). Note that /= always produces a float in Python, even between two integers — use //= for integer floor division.

This example applies each arithmetic compound-assignment operator and prints the intermediate result.

x = 10;  print(f"x = 10       → {x}")
 
# Addition assignment
x += 5;  print(f"x += 5       → {x}")
 
# Subtraction assignment
x -= 3;  print(f"x -= 3       → {x}")
 
# Multiplication assignment
x *= 2;  print(f"x *= 2       → {x}")
 
# Division assignment (always returns float)
x /= 4;  print(f"x /= 4       → {x}")
 
x = 10
# Floor division assignment
x //= 3; print(f"x //= 3      → {x}")
 
# Modulo assignment
x %= 2;  print(f"x %= 2       → {x}")
 
x = 2
# Exponentiation assignment
x **= 8; print(f"x **= 8      → {x}")
x = 10       → 10
x += 5       → 15
x -= 3       → 12
x *= 2       → 24
x /= 4       → 6.0
x //= 3      → 3
x %= 2       → 1
x **= 8      → 256

Compound bitwise assignment — in-place bit manipulation

Same bitwise compound operators as C#: &= masks, |= sets, ^= toggles, <<= shifts left, >>= shifts right. No >>>= (unsigned right shift) in Python since integers are arbitrary-width.

This example applies each bitwise compound-assignment operator and prints the resulting value.

x = 0b1100
x &= 0b1010; print(f"x &= 0b1010  → {x:04b}")
 
# Bitwise OR assignment — add/set bits (set flags)
x = 0b1100
x |= 0b1010; print(f"x |= 0b1010  → {x:04b}")
 
# Bitwise XOR assignment — toggle bits (flip flags)
x = 0b1100
x ^= 0b1010; print(f"x ^= 0b1010  → {x:04b}")
 
# Right shift assignment — divide by 2^n
x = 8
x >>= 2; print(f"x >>= 2      → {x}")
 
# Left shift assignment — multiply by 2^n
x <<= 3; print(f"x <<= 3      → {x}")
x &= 0b1010  → 1000
x |= 0b1010  → 1110
x ^= 0b1010  → 0110
x >>= 2      → 2
x <<= 3      → 16

Ternary, walrus, and precedence

Covers Python’s inline conditional expression, the walrus operator (:=) for assign-and-test patterns, and the full operator precedence table.

Ternary conditional expression — value if condition else alternative

Python’s ternary syntax is value_if_true if condition else value_if_false — note the reversed order compared to C#‘s condition ? true : false. Reads like English: “adult if age >= 18 else minor”.

This example assigns a value with Python’s ternary conditional expression.

age = 20
status = "adult" if age >= 18 else "minor"
print(f"age={age}{status}")
age=20 → adult

Nested ternary and walrus operator in expressions

Ternary expressions can be chained for multi-condition logic, though readability suffers beyond two levels — prefer if/elif/else for complex cases. The walrus operator := (Python 3.8+) assigns a value AND returns it in one expression, enabling compute-once-use-twice patterns in comprehensions and while loops. C# has no direct equivalent.

This example uses a nested ternary expression and a ternary inside an f-string.

score = 85
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D" if score >= 60 else "F"
print(f"score={score} → grade={grade}")
 
# Ternary in f-string
items = 3
print(f"You have {items} {'item' if items == 1 else 'items'}")
score=85 → grade=B
You have 3 items

Walrus operator in list comprehension:= assigns a value and returns it in the same expression, so y := x * 2 computes the doubled value, filters on > 10, and includes y in the result — all without a separate pre-comprehension variable.

This example uses the walrus operator inside a list comprehension and prints the filtered doubled values.

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
filtered = [y for x in data if (y := x * 2) > 10]
print(f"Doubled > 10: {filtered}")
Doubled > 10: [12, 14, 16, 18, 20]

Use the walrus operator in while loops for read-and-test patterns

The walrus operator shines in while loops that read input and test a condition: while (line := reader.readline()) reads a line, assigns it to line, and tests truthiness in one expression. Without :=, you need a priming read before the loop and a duplicate read at the end.

This example reads lines from a StringIO buffer until the stream is exhausted.

reader = io.StringIO("line1\nline2\nline3")
while (line := reader.readline()):
    print(f"  Read: {line.strip()}")
  Read: line1
  Read: line2
  Read: line3

Operator precedence — evaluation order from highest to lowest

Python evaluates operators in a strict precedence order. Parentheses bind tightest (level 1), the walrus operator binds loosest (level 14). Key difference from C#: ** (exponentiation) is level 2 and binds tighter than unary minus, so -2 ** 2 == -4 (not 4).

This example prints Python’s operator precedence table as a formatted multiline string.

precedence = """
  1. ()              Parentheses
  2. **              Exponentiation
  3. +x, -x, ~x     Unary plus, minus, bitwise NOT
  4. *, /, //, %     Multiplication, division, floor div, modulus
  5. +, -            Addition, subtraction
  6. <<, >>          Bitwise shifts
  7. &               Bitwise AND
  8. ^               Bitwise XOR
  9. |               Bitwise OR
 10. ==, !=, <, <=,  Comparisons, identity, membership
     >, >=, is, in
 11. not             Logical NOT
 12. and             Logical AND
 13. or              Logical OR
 14. :=              Walrus operator
"""
print(precedence.strip())
1. ()              Parentheses
  2. **              Exponentiation
  3. +x, -x, ~x     Unary plus, minus, bitwise NOT
  4. *, /, //, %     Multiplication, division, floor div, modulus
  5. +, -            Addition, subtraction
  6. <<, >>          Bitwise shifts
  7. &               Bitwise AND
  8. ^               Bitwise XOR
  9. |               Bitwise OR
 10. ==, !=, <, <=,  Comparisons, identity, membership
     >, >=, is, in
 11. not             Logical NOT
 12. and             Logical AND
 13. or              Logical OR
 14. :=              Walrus operator

Exponentiation and unary-minus precedence

A common precedence error is assuming -2 ** 2 means (-2) ** 2. Python evaluates it as -(2 ** 2) = -4 because ** binds tighter than unary -. Likewise, not 1 == 1 evaluates as not (1 == 1) because == has higher precedence than not.

This example compares grouped and ungrouped expressions to show Python’s precedence rules.

print(2 + 3 * 4)  # 14 (not 20)
print((2 + 3) * 4)  # 20
print(-2 ** 2)  # -4 (** binds tighter than unary -)
print((-2) ** 2)  # 4
print(not 1 == 1)  # False (== before not)
print(not (1 == 1))  # False (same, but explicit)
14
20
-4
4
False
False

Magic Methods (Dunder Methods)

Demonstrates how to implement Python’s “dunder” (double-underscore) methods for custom operator support, iteration, context management, and introspection. These are Python’s equivalent of C#‘s operator overloading, IEnumerable, IDisposable, and reflection.

Custom operator implementation

Shows how to implement the full suite of dunder methods on a Vector class, enabling natural Python syntax for arithmetic, comparison, iteration, and context management.

Define a Vector class with dunder methods for operators, iteration, and hashing

Implement dunder methods for full operator support: __add__/__sub__/__mul__ for arithmetic, __eq__/__hash__ for equality, __getitem__/__len__/__iter__ for collection integration. Natural syntax: v1 + v2, abs(v1), v1[0], len(v1). Use for mathematical types (vectors, matrices, coordinates) where operators have clear meaning.

Anti-patterns

  • __eq__ without __hash__ — breaks dict/set usage
  • Non-intuitive operator semantics+ should mean addition
  • Mutable objects with __hash__ — hash changes corrupt collections

Correct pattern

Always define __hash__ alongside __eq__. Keep __hash__ consistent: return hash((self.x, self.y)). Use __eq__ + __hash__ together so instances work correctly as dict keys and in sets.

This example defines a Vector type that supports arithmetic, indexing, hashing, iteration, and truthiness.

class Vector:
    # __init__: constructor
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    # __str__: human-readable string)
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
    # __repr__: developer/debug string
    def __repr__(self):
        return f"Vector(x={self.x}, y={self.y})"
 
    # __eq__: equality / operator ==)
    def __eq__(self, other):
        return isinstance(other, Vector) and self.x == other.x and self.y == other.y
 
    # __hash__: hashing for sets/dicts)
    def __hash__(self):
        return hash((self.x, self.y))
 
    # __add__: + operator
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
 
    # __sub__: - operator
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
 
    # __mul__: * operator
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
 
    # __neg__: unary -
    def __neg__(self):
        return Vector(-self.x, -self.y)
 
    # __abs__: abs() function
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
 
    # __len__: len() function
    def __len__(self):
        return 2  # 2D vector
 
    # __getitem__: indexing obj[i]
    def __getitem__(self, index):
        if index == 0: return self.x
        if index == 1: return self.y
        raise IndexError("Vector index out of range")
 
    # __contains__: 'in' operator)
    def __contains__(self, value):
        return value == self.x or value == self.y
 
    # __lt__, __le__, __gt__, __ge__: comparison operators
    def __lt__(self, other):
        return abs(self) < abs(other)
 
    # __bool__: truthiness
    def __bool__(self):
        return self.x != 0 or self.y != 0
 
    # __call__: make object callable like a function
    def __call__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
 
    # __iter__: make iterable
    def __iter__(self):
        yield self.x
        yield self.y

Use str() and repr() for human-readable and developer representations

__str__ is called by print() and str() — for end users. __repr__ is called by the REPL, debugger, and repr() — for developers. Best practice: __repr__ should return a string that could recreate the object. If only one is defined, define __repr__ — Python falls back to it when __str__ is missing.

This example prints the Vector object with both str() and repr().

v1 = Vector(3, 4)
v2 = Vector(1, 2)
 
print(str(v1))  # __str__
print(repr(v1))  # __repr__
Vector(3, 4)
Vector(x=3, y=4)

Test arithmetic, equality, and comparison via dunder methods

Once dunder methods are defined, instances support natural Python syntax: v1 + v2 calls __add__, abs(v1) calls __abs__, v1 == v2 calls __eq__, and v1 < v2 calls __lt__. hash() calls __hash__ — required for using instances as dict keys or set members.

This example exercises the arithmetic, equality, comparison, and hashing dunder methods on Vector.

print(v1 + v2)  # __add__
print(v1 - v2)  # __sub__
print(v1 * 3)  # __mul__
print(-v1)  # __neg__
print(abs(v1))  # __abs__
 
print(v1 == v2)  # __eq__
print(f"v1 == Vector(3,4): {v1 == Vector(3, 4)}")
print(v1 < v2)  # __lt__ (compares magnitude)
print(hash(v1))  # __hash__
Vector(4, 6)
Vector(2, 2)
Vector(9, 12)
Vector(-3, -4)
5.0
False
v1 == Vector(3,4): True
False
1079245023883434373

Access by index, iterate, test membership, and call as a function

__getitem__ enables v1[0], __len__ enables len(v1), __iter__ enables for x in v1 and list(v1), __contains__ enables 3 in v1, __call__ makes the instance callable like a function (v1(2)), and __bool__ controls truthiness (if v1:).

This example indexes, iterates, tests membership, calls, and checks truthiness on a Vector.

print(v1[0])  # __getitem__
print(v1[1])   # v1[1]
print(3 in v1)  # __contains__
print(5 in v1)   # 5 in v1
print(len(v1))  # __len__
 
print(v1(2))  # __call__
print(list(v1))  # __iter__
print(bool(v1))  # __bool__
print(bool(Vector(0, 0)))   # bool(Vector(0,0))
3
4
True
False
2
Vector(6, 8)
[3, 4]
True
False

Implement a context manager with enter and exit

Context managers enable the with statement for automatic resource cleanup — Python’s equivalent of C#‘s using statement and IDisposable. __enter__ runs at the start of the with block (return value is bound to as). __exit__ runs when the block ends (even on exception). Return False from __exit__ to propagate exceptions, True to suppress them.

This example implements a context manager that prints enter and exit messages around a timed block.

class Timer:
    """Context manager that times a block of code"""
    def __enter__(self):
        self.start = _time.perf_counter()
        print("Timer started")
        return self
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = _time.perf_counter() - self.start
        print("Timer stopped")
        return False
 
with Timer() as t:
    total = sum(range(1_000_000))
    print(f"Sum: {total}")
Timer started
Sum: 499999500000
Timer stopped

Type inspection and reflection

Covers Python’s introspection system — magic attributes on modules, functions, and classes that expose type hierarchy, method resolution order, and metadata at runtime.

Inspect module identity and docstrings with name and doc

Every module has __name__ (its import name, or "__main__" for the entry-point script) and __doc__ (its docstring). The if __name__ == "__main__" guard prevents code from running when the module is imported as a library.

This example prints a module name and a short preview of a module docstring.

print(__name__)
print(math.__name__)   # math.__name__
print(f"math.__doc__[:50]: {(math.__doc__ or '')[:50]}...")
__main__
math
math.__doc__[:50]: This module provides access to the mathematical fu...

Introspect object type with class, type(), isinstance(), and dict

__class__ and type() both return the type of an instance. isinstance() checks the type hierarchy (including parent classes). __dict__ exposes the object’s attributes as a dictionary — this is Python’s equivalent of C# reflection on instance fields.

This example prints the runtime type, class name, and instance dictionary for a Vector.

v = Vector(3, 4)
print(v.__class__)   # v.__class__
print(f"v.__class__.__name__: {v.__class__.__name__}")
print(type(v))   # type(v)
print(isinstance(v, Vector))   # isinstance(v, Vector)
print(v.__dict__)   # v.__dict__
<class '__main__.Vector'>
v.__class__.__name__: Vector
<class '__main__.Vector'>
True
{'x': 3, 'y': 4}

Optimize memory with slots — fixed attribute storage

__slots__ replaces the per-instance __dict__ with a fixed-size tuple of attribute descriptors. This reduces memory usage by 30–50% for classes with many instances (e.g., millions of data points). The tradeoff: you cannot add attributes dynamically. Use for data-heavy classes; avoid for classes that need runtime attribute flexibility.

This example shows that __slots__ blocks new attributes and removes the normal instance dictionary.

class Point:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
p = Point(1, 2)
print(f"Point({p.x}, {p.y})")
try:
    p.z = 3
except AttributeError:
    print("p.z = 3 → AttributeError (slots restricts attributes)")
print(f"Has __dict__? {hasattr(p, '__dict__')}")
Point(1, 2)
p.z = 3 → AttributeError (slots restricts attributes)
Has __dict__? False

Complete dunder method reference by category

Comprehensive list of all major dunder method categories. Each category enables a different aspect of Python’s protocol system — implementing the right dunders lets your class integrate seamlessly with built-in functions, operators, and control flow.

This example prints the main dunder-method categories and the special methods in each group.

categories = {
    "Creation/Destroy": "__init__, __new__, __del__",
    "String":           "__str__, __repr__, __format__",
    "Comparison":       "__eq__, __ne__, __lt__, __le__, __gt__, __ge__",
    "Arithmetic":       "__add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__",
    "Reverse Arith":    "__radd__, __rsub__, __rmul__ (when left operand doesn't support it)",
    "In-place Arith":   "__iadd__, __isub__, __imul__ (+=, -=, *=)",
    "Unary":            "__neg__, __pos__, __abs__, __invert__",
    "Type Conversion":  "__int__, __float__, __bool__, __complex__, __bytes__",
    "Container":        "__len__, __getitem__, __setitem__, __delitem__, __contains__",
    "Iteration":        "__iter__, __next__, __reversed__",
    "Callable":         "__call__",
    "Context Manager":  "__enter__, __exit__",
    "Attribute Access": "__getattr__, __setattr__, __delattr__, __getattribute__",
    "Hashing":          "__hash__",
    "Descriptor":       "__get__, __set__, __delete__",
}
for cat, methods in categories.items():
    print(f"  {cat:20}: {methods}")
  Creation/Destroy    : __init__, __new__, __del__
  String              : __str__, __repr__, __format__
  Comparison          : __eq__, __ne__, __lt__, __le__, __gt__, __ge__
  Arithmetic          : __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__
  Reverse Arith       : __radd__, __rsub__, __rmul__ (when left operand doesn't support it)
  In-place Arith      : __iadd__, __isub__, __imul__ (+=, -=, *=)
  Unary               : __neg__, __pos__, __abs__, __invert__
  Type Conversion     : __int__, __float__, __bool__, __complex__, __bytes__
  Container           : __len__, __getitem__, __setitem__, __delitem__, __contains__
  Iteration           : __iter__, __next__, __reversed__
  Callable            : __call__
  Context Manager     : __enter__, __exit__
  Attribute Access    : __getattr__, __setattr__, __delattr__, __getattribute__
  Hashing             : __hash__
  Descriptor          : __get__, __set__, __delete__

Explore module magic attributes — name, file, spec

Every Python module exposes metadata attributes. __file__ is the source file path (unavailable for C extensions and notebooks). __spec__ (PEP 451) holds import system metadata: name, loader, origin path, and submodule search locations. Built-in C extension modules (like math) may lack __file__ but still have __name__ and __doc__.

This example prints stable summaries for module names, docstrings, file paths, and import metadata.

doc_preview = (os.__doc__ or "").replace("\n", " ")[:60]
math_doc_preview = (math.__doc__ or "").replace("\n", " ")[:60]
 
print(__name__)
print(f"__doc__:       {doc_preview}...")
print(math.__name__)
print(f"math.__doc__:  {math_doc_preview}...")
print(getattr(math, '__file__', 'N/A (built-in C module)'))
print(os.__name__)
print(os.__file__)
print(json.__file__)
print(f"json.__spec__.name: {json.__spec__.name}")
print(f"json.__spec__.origin: {json.__spec__.origin}")
__main__
__doc__:       OS routines for NT or Posix depending on what system we're o...
math
math.__doc__:  This module provides access to the mathematical functions de...
N/A (built-in C module)
os
C:\Users\aperi\AppData\Local\Programs\Python\Python312\Lib\os.py
C:\Users\aperi\AppData\Local\Programs\Python\Python312\Lib\json\__init__.py
json.__spec__.name: json
json.__spec__.origin: C:\Users\aperi\AppData\Local\Programs\Python\Python312\Lib\json\__init__.py

Inspect function metadata — name, doc, annotations, defaults

Functions are first-class objects with their own magic attributes. __annotations__ holds type hints as a dict. __defaults__ holds default argument values as a tuple. __qualname__ gives the fully qualified name — for nested functions it shows the enclosing scope (e.g., Outer.inner).

This example prints the key metadata attributes exposed by a Python function object.

def greet(name: str, greeting: str = "Hello") -> str:
    """Returns a greeting message."""
    return f"{greeting}, {name}!"
 
print(greet.__name__)
print(greet.__doc__)
print(greet.__module__)
print(greet.__defaults__)
print(f"__annotations__: {greet.__annotations__}")
print(greet.__qualname__)
 
class Animal:
    """Base class for animals."""
    pass
greet
Returns a greeting message.
__main__
('Hello',)
__annotations__: {'name': <class 'str'>, 'greeting': <class 'str'>, 'return': <class 'str'>}
greet

Define a class hierarchy for attribute inspection

Setting up a simple AnimalDog hierarchy to demonstrate class and instance magic attributes.

This example defines a simple Animal and Dog class hierarchy for later introspection.

class Dog(Animal):
    """A dog."""
    species = "canine"
    def __init__(self, name):
        self.name = name

Walk the class hierarchy with bases, mro, and dict

__bases__ lists direct parent classes. __mro__ (Method Resolution Order) shows the full inheritance chain Python follows when resolving attribute lookups — equivalent to C#‘s Type.BaseType chain. __dict__ on a class shows the class namespace (methods, class variables); on an instance it shows instance attributes only.

This example prints the inheritance metadata and instance attributes for the Dog class.

d = Dog("Rex")
print(Dog.__name__)  # 'Dog'
print(Dog.__doc__)  # 'A dog.'
print(Dog.__module__)  # '__main__'
print(Dog.__bases__)  # (Animal,) — direct parent classes
print(Dog.__mro__)  # method resolution order
print(f"Dog.__dict__.keys: {list(Dog.__dict__.keys())}")
 
print(d.__class__)  # <class 'Dog'>
print(f"d.__class__.__name__: {d.__class__.__name__}")
print(d.__dict__)  # {'name': 'Rex'} — instance attributes
print(f"{d.__sizeof__()} bytes")
 
print(type(d).__name__)  # 'Dog'
print(isinstance(d, Dog))   # isinstance(d, Dog)
print(isinstance(d, Animal))   # isinstance(d, Animal)
print(issubclass(Dog, Animal))   # issubclass(Dog, Animal)
Dog
A dog.
__main__
(<class '__main__.Animal'>,)
(<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)
Dog.__dict__.keys: ['__module__', '__doc__', 'species', '__init__']
<class '__main__.Dog'>
d.__class__.__name__: Dog
{'name': 'Rex'}
16 bytes
Dog
True
True
True

Magic attribute quick reference

These attributes are present on all modules, classes, and functions and expose read-only metadata.

AttributePurpose
__name__Module name / class name / function name
__doc__Docstring of any object
__dict__Attributes as a dict (class or instance)
__class__Type of an instance
__bases__Parent classes
__mro__Method resolution order (inheritance chain)
__file__Source file path (pure Python modules only)
__annotations__Type hints on a function
__defaults__Default argument values

These are read-only metadata — not methods you override.

Mutable vs Immutable Types Reference

Quick-reference section for Python’s mutable/immutable distinction — which types are safe as dict keys, dataclass defaults, and shared state, and which require copies, factories, or synchronization.

Operational semantics

Python’s key distinction is between mutable and immutable objects, not between value types and reference types. Immutable operations allocate a new object; mutable operations update the existing object in place.

This subsection explains how that distinction affects aliasing, hashing, dataclass defaults, and shared state across call boundaries.

Assignment

When an immutable value appears to change, Python creates a new object and rebinds the name, so existing aliases continue to reference the earlier value. Mutable objects behave differently: in-place operations update the shared object, and every alias observes the change immediately.

Function arguments

Passing an immutable object into a function is operationally safe when the function only rebinds the local parameter, because the caller’s object is unchanged. Passing a mutable object transfers a reference to the same object, so in-place mutation inside the function is visible to the caller.

Dict keys and set elements

Immutable values with stable equality and hash behavior can participate in dictionary keys and set membership. Mutable built-in containers such as list, dict, and set cannot, because their contents can change after insertion and would invalidate hash-based lookups.

Dataclass defaults

Immutable literal defaults are usually safe because the value itself does not change in place across instances. Mutable defaults must be created with field(default_factory=...) so each instance receives an independent object instead of sharing hidden state.

Shared state

Immutable values are easier to share across scopes and threads because readers cannot observe in-place mutation. Mutable values require copying, ownership boundaries, or explicit synchronization when several call sites can mutate the same object.

Type reference

This subsection lists the common immutable and mutable types that matter most when evaluating hashability, sharing behavior, and safe default construction.

Immutable type reference

These common immutable types are safe to share and, where hash semantics permit, can participate in dict and set keys.

TypeExampleOperational note
int42Arbitrary precision integer
float3.14IEEE 754 double-precision float
complex3+4jBuilt-in complex number
boolTrueSubclass of int
str"hello"Unicode text sequence
bytesb"hello"Immutable byte sequence
tuple(1, 2, 3)Ordered immutable container
frozensetfrozenset({1, 2})Immutable set
rangerange(10)Immutable arithmetic sequence
NoneNoneSingleton null object
DecimalDecimal("3.14")Exact base-10 numeric type
FractionFraction(1, 3)Exact rational value
namedtuplePoint(3, 4)Tuple with named fields

Mutable type reference

These mutable types need factories, copies, or coordination when shared across instances or threads.

TypeExampleSafe dataclass default
list[1, 2, 3]field(default_factory=list)
dict{"a": 1}field(default_factory=dict)
set{1, 2, 3}field(default_factory=set)
bytearraybytearray(b"hi")field(default_factory=bytearray)
dequedeque([1, 2])field(default_factory=deque)
defaultdictdefaultdict(int)field(default_factory=lambda: defaultdict(int))
User-defined mutable classMyClass()field(default_factory=MyClass)

Runtime examples

Demonstrates the mutability rules with runnable examples for reassignment, aliasing, hashability, and function calls.

Immutable reassignment — strings create new objects

String += produces a new object, leaving the original reference unchanged. The identity check confirms that a and b no longer reference the same string object.

This example shows that string concatenation rebinds the name instead of mutating the original object.

a = "hello"
b = a
a += " world"
print(f"a = {a!r}")
print(f"b = {b!r}")
print(f"Same object? {a is b}")
a = 'hello world'
b = 'hello'
Same object? False

Mutable shared reference — lists modify in place

Assigning a list to another variable copies the reference, not the data. Both variables point to the same list object — append() through either variable is visible through the other. Use b = a.copy() or b = list(a) for a shallow copy.

This example appends through one list alias and shows that the second alias sees the same mutation.

a = [1, 2, 3]
b = a
a.append(4)
print(f"a = {a}")
print(f"b = {b}")
print(f"Same object? {a is b}")
a = [1, 2, 3, 4]
b = [1, 2, 3, 4]
Same object? True

Dict keys must be hashable — only immutable types allowed

Only objects with a stable __hash__ implementation can be dict keys or set members. Mutable built-in containers (list, set, dict) are not hashable — attempting to use them as keys raises TypeError. Convert to immutable equivalents such as tuple and frozenset.

This example builds a dictionary with immutable key types, including a tuple and a frozenset.

d = {}
d["string_key"] = 1
d[(1, 2)] = 2
d[frozenset({3})] = 3
print(f"Dict: {d}")
Dict: {'string_key': 1, (1, 2): 2, frozenset({3}): 3}

Demonstrate mutable function arguments — caller’s object is modified

When a mutable object is passed to a function, the function receives a reference to the same object. Mutations inside the function (.append(), [key]=value) affect the caller’s variable. To prevent modification, pass a copy such as add_item(my_list.copy(), 3).

This example mutates a list inside a function and then prints the caller’s modified list.

def add_item(lst, item):
    lst.append(item)
 
my_list = [1, 2]
add_item(my_list, 3)
print(f"my_list: {my_list}")
my_list: [1, 2, 3]

Workload Selection

Summarizes the workload profiles where Python is operationally strong and the cases where its runtime model or ecosystem constraints justify a different implementation strategy.

Appropriate workloads

Python is strongest where developer throughput, library breadth, and I/O orchestration matter more than raw per-core execution speed.

Interactive analysis and notebooks

Python is a strong default for exploratory work because the REPL, notebooks, and data libraries shorten the loop between hypothesis, inspection, and revision. That matters when analysts or engineers need to profile payloads, validate assumptions, or iterate on transformations before the logic is hardened into a service or scheduled pipeline.

This example aggregates a small in-memory sample the way an exploratory notebook cell would before a pipeline is formalized.

sample = [
    {"language": "python", "rows": 1200},
    {"language": "python", "rows": 800},
    {"language": "sql", "rows": 300},
]
python_rows = sum(item["rows"] for item in sample if item["language"] == "python")
print(f"python_rows={python_rows}")
print(f"entries={len(sample)}")
python_rows=2000
entries=3

Data movement, orchestration, and automation

Python fits control-plane work well: pulling data from APIs, moving files, coordinating database operations, and driving scheduled jobs. The language spends most of its time at I/O boundaries in these workloads, so helpers such as pathlib.Path matter more than interpreter overhead.

This example builds a small file handoff list for an automation step.

from pathlib import Path
 
files = [Path("incoming/orders.json"), Path("archive/orders-2026-04-15.json")]
for path in files:
    print(path.as_posix())
incoming/orders.json
archive/orders-2026-04-15.json

I/O-bound services and tooling

Python remains effective for services and utilities that wait on networks, disks, subprocesses, or remote systems more than they saturate CPU cores. asyncio, mature client libraries, and concise scripting syntax make it practical for integration-heavy tooling and service endpoints.

This example overlaps two waiting tasks with asyncio.gather() to show the I/O-bound case where Python stays efficient.

import asyncio
import time
 
async def fetch(tag, delay):
    await asyncio.sleep(delay)
    return f"{tag}:{delay:.2f}"
 
async def main():
    return await asyncio.gather(fetch("api", 0.05), fetch("disk", 0.05))
 
start = time.perf_counter()
result = asyncio.run(main())
elapsed = time.perf_counter() - start
print(", ".join(result))
print(f"elapsed={elapsed:.3f}s")
api:0.05, disk:0.05
elapsed=0.054s

Rapid iteration on heterogeneous data

When schemas are still evolving or inputs arrive in several formats, Python’s dynamic object model reduces the ceremony required to inspect, normalize, and reshape the data. That is useful during early pipeline design, one-off migrations, and operational tooling that must tolerate uneven source quality with simple strip() and float() cleanup steps.

This example normalizes mixed-shape records without introducing framework scaffolding first.

records = [
    {"id": "  A-01 ", "amount": "19.99"},
    {"id": "B-02", "amount": 7},
]
normalized = [
    {"id": row["id"].strip().lower(), "amount": float(row["amount"])}
    for row in records
]
print(normalized)
[{'id': 'a-01', 'amount': 19.99}, {'id': 'b-02', 'amount': 7.0}]

Glue code around optimized engines

Python works well as the orchestration layer around systems that do the heavy compute elsewhere, such as sqlite3, NumPy, Polars, vectorized database execution, and external services. In that role, Python coordinates the workflow while optimized engines handle the expensive inner loops.

This example lets SQLite compute the aggregate while Python owns the orchestration around it.

import sqlite3
 
conn = sqlite3.connect(":memory:")
conn.execute("create table orders(total real)")
conn.executemany("insert into orders(total) values (?)", [(19.5,), (22.0,), (8.5,)])
total = conn.execute("select sum(total) from orders").fetchone()[0]
print(f"sql_total={total:.1f}")
conn.close()
sql_total=50.0

Constraints and trade-offs

The following constraints matter when deciding whether Python should remain the primary runtime in production.

CPU-bound hot loops

Pure Python loops pay interpreter overhead on every iteration, so tight numeric or per-record transformations often become throughput bottlenecks. When the hot path is compute-heavy, move the repeated sum() or vector math into NumPy, Polars, Cython, a native extension, or another runtime such as C#.

This example shows a pure-Python numeric loop that is fast enough for a demo but still pays per-iteration interpreter cost.

import time
 
start = time.perf_counter()
total = sum(i * i for i in range(200000))
elapsed = time.perf_counter() - start
print(f"checksum={total}")
print(f"elapsed={elapsed:.3f}s")
checksum=2666646666700000
elapsed=0.015s

Memory-constrained workloads

Python objects carry significant per-object overhead compared with packed arrays or lower-level runtimes. If the workload is dominated by millions of small objects, prefer vectorized structures, columnar engines, or compact containers such as array('I') that give tighter control over memory layout.

This example compares a Python list with a packed array('I') holding the same number of integers.

import sys
from array import array
 
ints = [0] * 1000
packed = array("I", [0] * 1000)
print(f"list_bytes={sys.getsizeof(ints)}")
print(f"array_bytes={sys.getsizeof(packed)}")
list_bytes=8056
array_bytes=4080

Compile-time type guarantees

Python detects most type mismatches only when the code executes. Type hints, mypy, and pyright improve safety substantially, but they remain optional tooling layers rather than hard compile-time enforcement.

This example shows that an annotated function still fails only when the incompatible call reaches runtime.

def add_one(value: int) -> int:
    return value + 1
 
try:
    print(add_one("1"))
except TypeError as exc:
    print(type(exc).__name__)
    print(exc)
TypeError
can only concatenate str (not "int") to str

Desktop and mobile GUI applications

Python can support GUI work through libraries such as tkinter, but its ecosystem is weaker than the primary .NET and platform-native stacks for long-lived desktop or mobile applications. Choose Python only when the surrounding problem is primarily scripting or data integration and the UI surface is secondary.

This example checks the bundled Tk runtime that backs the standard-library desktop GUI path.

import tkinter as tk
 
print(f"tk_version={tk.TkVersion}")
print(f"tcl_version={tk.TclVersion}")
tk_version=8.6
tcl_version=8.6

CPU parallelism with threads

In CPython, the GIL prevents parallel execution of Python bytecode across CPU threads. Threads remain useful for I/O concurrency, but CPU-bound scaling requires multiprocessing, vectorized or native libraries, or a runtime without that execution model.

This example shows threads overlapping waiting work under cpython, which is useful for I/O but not evidence of CPU-bound speedup.

import sys
import time
from concurrent.futures import ThreadPoolExecutor
 
def wait_task(tag):
    time.sleep(0.05)
    return tag
 
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
    result = list(pool.map(wait_task, ["thread-a", "thread-b"]))
elapsed = time.perf_counter() - start
print(", ".join(result))
print(f"implementation={sys.implementation.name}")
print(f"elapsed={elapsed:.3f}s")
thread-a, thread-b
implementation=cpython
elapsed=0.051s

Engineering Practices

These practices keep basic Python code predictable in production and reduce the most common correctness defects.

String formatting and sentinel checks

These conventions keep basic value formatting and sentinel handling explicit and predictable.

Prefer f-strings in new code

Use f-strings when the template is written inline with the code because they keep the literal text and interpolated expressions in one place. Prefer .format() only when the format string itself is dynamic or externally supplied.

This example formats a literal template with an inline f-string.

name = "Alice"
total = 19.5
message = f"{name} owes {total:.2f}"
print(message)
Alice owes 19.50

Use is None for sentinel checks

None is a singleton, so identity testing expresses the intended semantics directly and avoids surprises from overloaded equality methods. Reserve == for value comparison between objects that meaningfully participate in equality.

This example separates singleton checks from ordinary value comparison.

def choose_limit(limit=None):
    if limit is None:
        return 100
    return limit
 
print(choose_limit())
print(choose_limit(25))
print(None is None)
100
25
True

Environment and interface discipline

These conventions reduce dependency drift and make public contracts easier to validate during review and in CI.

Keep dependencies inside a virtual environment

Install project packages into the active virtual environment instead of the system interpreter, and verify both sys.executable and VIRTUAL_ENV before you install anything. That isolates dependency graphs across projects and prevents toolchain drift from breaking unrelated work.

This example records the interpreter path and active virtual environment before dependency installation.

import os
import sys
 
print(sys.executable)
print(os.environ.get("VIRTUAL_ENV", "None"))
C:\Users\aperi\My Drive\VAULT\.vault\Scripts\python.exe
None

Add type hints to public APIs

Type hints improve code review, editor support, and refactor safety by making the intended contract explicit. Validate those contracts in CI with mypy, pyright, or an equivalent checker rather than leaving them as unenforced comments.

This example annotates a public helper with explicit input and return types.

def normalize_id(value: str | None) -> str | None:
    return value.strip().lower() if value else None
 
print(normalize_id("  AbC-42  "))
print(normalize_id(None))
abc-42
None

Runtime correctness and object semantics

These conventions address the cases where Python’s runtime model most often causes correctness defects in production code.

Use Decimal for currency and tax rules

Use Decimal when the domain requires exact base-10 behavior, and construct values from strings rather than floats. That preserves the intended decimal value instead of inheriting a binary floating-point approximation.

This example performs currency arithmetic with exact decimal values.

from decimal import Decimal
 
subtotal = Decimal("19.99")
tax_rate = Decimal("0.21")
print(subtotal * tax_rate)
4.1979

Apply __slots__ only to stable, high-volume classes

__slots__ can reduce per-instance memory and block accidental attribute creation, but it also removes normal instance dictionaries and changes inheritance behavior. Use it only when the class shape is stable and the memory savings are operationally relevant.

This example defines a fixed-shape object without an instance dictionary.

class Point:
    __slots__ = ("x", "y")
 
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
point = Point(1, 2)
print(hasattr(point, "__dict__"))
False

Prefer targeted EAFP

EAFP remains idiomatic in Python, but the exception handler must be narrow enough to distinguish expected control-flow failures from real defects. Catch only the exception type that the operation can legitimately raise.

This example catches only the conversion failure that int() is expected to raise.

value = "42"
 
try:
    age = int(value)
except ValueError:
    age = None
 
print(age)
42

Pair __eq__ with __hash__ on value objects

If a class defines value-based equality and instances are meant to participate in sets or dictionary keys, review __hash__ at the same time. Base hashing only on immutable fields so equality and hash stability remain aligned over the object’s lifetime.

This example keeps equality and hashing aligned so instances work in sets.

class UserId:
    def __init__(self, value):
        self.value = value
 
    def __eq__(self, other):
        return isinstance(other, UserId) and self.value == other.value
 
    def __hash__(self):
        return hash(self.value)
 
ids = {UserId(7)}
print(UserId(7) in ids)
True

Common Failure Modes

Consolidates the recurring defects that appear in introductory Python code and the safe patterns that prevent them.

Shared-state and aliasing defects

These entries cover the failure modes where object reuse or reference sharing produces hidden state changes.

Mutable default arguments

Default argument values are evaluated once at function definition time, not once per call. A mutable default therefore becomes shared state across invocations and accumulates prior mutations unless you use a sentinel such as None and allocate inside the function body.

This example contrasts a shared default list with a per-call allocation pattern.

def bad_append(item, bucket=[]):
    bucket.append(item)
    return bucket
 
def good_append(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket
 
print(bad_append(1))
print(bad_append(2))
print(good_append(1))
print(good_append(2))
[1]
[1, 2]
[1]
[2]

Shared mutable references

Assigning a list or dictionary to another variable copies the reference, not the underlying object. Mutating through either name changes the same shared object unless you explicitly create a shallow or deep copy.

This example shows the difference between aliasing the same list and copying it.

a = [1, 2]
b = a
c = a.copy()
b.append(3)
 
print(a)
print(b)
print(c)
[1, 2, 3]
[1, 2, 3]
[1, 2]

Numeric and comparison defects

These entries cover the cases where value comparison and numeric representation diverge from naive expectations.

Float equality

Binary floating-point cannot represent many decimal fractions exactly, so direct equality checks across computed floats are brittle. Use math.isclose() for tolerance-based comparison or Decimal when the domain requires exact decimal semantics.

This example compares direct float equality with tolerance-based comparison.

import math
 
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))
False
True

Identity used for value comparison

Identity answers whether two names point to the same object, not whether two values are equal. is can appear to work for some literals because of interning, but that behavior is incidental and must not be used as application logic.

This example separates value equality from object identity.

x = [1, 2]
y = [1, 2]
 
print(x == y)
print(x is y)
print(None is None)
True
False
True

Decimal built from float

Decimal(0.1) preserves the float approximation that already exists in memory, which defeats the purpose of moving to exact decimal arithmetic. Construct the value from a string or integer inputs so the decimal representation starts exact.

This example shows that Decimal(0.1) is not equivalent to Decimal("0.1").

from decimal import Decimal
 
print(Decimal(0.1) == Decimal("0.1"))
print(Decimal("0.1") == Decimal("0.1"))
False
True

Exception-handling defects

These entries cover patterns where Python’s concise exception syntax is used too broadly and starts hiding real failures.

Over-broad exception handling in EAFP code

EAFP does not justify catching every exception class around a risky operation. A broad except Exception block suppresses unrelated defects and makes production diagnostics materially harder.

This example shows a broad handler swallowing an unrelated defect that a narrow handler would not hide.

def parse_age_bad(value):
    try:
        return int(value) / 0
    except Exception:
        return "hidden"
 
def parse_age_good(value):
    try:
        return int(value)
    except ValueError:
        return "invalid"
 
print(parse_age_bad("12"))
print(parse_age_good("x"))
hidden
invalid

Python Basics Troubleshooting

Maps common runtime and language errors to their immediate cause and first corrective action.

Collection and conversion errors

These errors usually come from invalid value shapes, missing returns, or unsafe coercion at runtime boundaries.

Unhashable list used as a dict key or set element

This error means a mutable container such as list was used where Python requires a stable hash value. Convert the key material to an immutable equivalent such as tuple or frozenset before inserting it into a dictionary or set.

This example converts list-based key material to a tuple before insertion.

tags = ["python", "data"]
lookup = {tuple(tags): "ok"}
print(lookup[("python", "data")])
ok

None returned where iteration expects a collection

This usually means the function returned None implicitly because execution reached the end of the function without an explicit return. Verify the branch structure and ensure every successful path returns the collection type that the caller expects.

This example returns an empty list instead of allowing an implicit None.

def get_items(flag):
    if flag:
        return [1, 2]
    return []
 
print(get_items(False))
[]

Invalid integer literal passed to int()

int() accepts only strings that already represent integers in the expected format. Wrap the conversion in try/except ValueError when the input originates from users, files, or external systems that can produce malformed values.

This example catches malformed numeric input at the conversion boundary.

value = "12x"
 
try:
    parsed = int(value)
except ValueError:
    parsed = None
 
print(parsed)
None

Scope and binding errors

These errors come from name resolution and Python’s local-binding rules inside function bodies.

Name used before assignment

NameError indicates that the name does not exist in the current scope at the point where Python evaluates it. Check spelling first, then verify the variable is bound before the reference and that the code path actually executed the binding statement.

This example binds the name before it is referenced.

x = 1
print(x)
1

Local rebinding causes UnboundLocalError

This error occurs when a function assigns to a name that also exists in an outer scope and then tries to read the name before the local assignment has run. Rename the local variable or declare nonlocal or global only when shared state is truly intended.

This example uses nonlocal to make the binding intent explicit inside a nested function.

def outer():
    total = 1
 
    def inner():
        nonlocal total
        total += 2
        return total
 
    return inner()
 
print(outer())
3

Interpreter and environment errors

These errors point to runtime-version mismatches or packages being resolved from the wrong interpreter context.

Walrus operator used on a pre-3.8 interpreter

Assignment expressions were introduced in Python 3.8, so earlier interpreters treat := as invalid syntax. Confirm the active interpreter with sys.version and either upgrade the runtime or rewrite the expression as a separate assignment statement.

This example checks whether the active interpreter supports assignment expressions.

import sys
 
print(sys.version_info >= (3, 8))
True

Package missing from the active virtual environment

ModuleNotFoundError usually means the dependency was installed into a different interpreter or virtual environment from the one currently running the program. Activate the intended environment first, then install the package there and verify with sys.executable if necessary.

This example captures the interpreter and active virtual environment before package installation.

import os
import sys
 
print(sys.executable)
print(os.environ.get("VIRTUAL_ENV", "None"))
C:\Users\aperi\My Drive\VAULT\.vault\Scripts\python.exe
None

Protected interpreter installation or incorrect environment activation

PermissionError during import or package management usually points to a protected system interpreter, a locked path, or an operation being attempted outside the intended virtual environment. Activate the project environment and avoid sudo pip or equivalent system-wide package mutations.

This example prints the exact interpreter-owned venv command to use instead of mutating a protected installation.

import sys
from pathlib import Path
 
target = Path(".venv").resolve()
print(sys.executable)
print(f"{sys.executable} -m venv {target}")
C:\Users\aperi\My Drive\VAULT\.vault\Scripts\python.exe
C:\Users\aperi\My Drive\VAULT\.vault\Scripts\python.exe -m venv C:\Users\aperi\My Drive\VAULT\.venv

Points to adjacent material when the core language types start affecting serialization and pipeline design decisions.

Adjacent references

  • Data Architecture: SerializationSerialization Formats for when bytes, JSON, and Parquet choices matter in pipelines