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)
Summary
Python foundations reference covering runtime validation, console I/O, core data types, operator behavior, dunder methods, and mutability semantics.
Runtime and environment
- Verify interpreter identity with
sys.versionandsys.executable- Confirm the active virtual environment with
os.environ.get("VIRTUAL_ENV")- Inspect installed distributions with
importlib.metadata.distributions()and fail fast on missing imports with__import__()Console I/O and formatting
print()controls output withsep,end,file, andflush- Prefer f-strings; use
.format()for dynamic templates and%formatting only for legacy codeinput()always returnsstr; parse withint()orfloat()insidetry/except ValueErrorrepr()exposes developer-oriented representations;str()produces user-facing outputData model
- Names bind to objects at runtime; rebinding changes the reference, not the object
intis arbitrary precision;floatfollows IEEE 754;Decimalprovides exact base-10 arithmeticboolis a subclass ofint;Noneis a singleton;bytesandbytearrayseparate immutable and mutable binary datadefaultdict,Counter,namedtuple,deque,memoryview, andFractioncover common specialized casesOperators and protocols
- Arithmetic, comparison, identity, membership, logical, bitwise, ternary, and walrus operators are covered with runnable examples
andandorreturn operands, not coerced booleans- Dunder methods define arithmetic, hashing, iteration, context management, and introspection behavior
Mutability and operational safety
- Immutable objects are safe as dict keys and dataclass defaults; mutable objects require copies or factories
- Common defects include mutable default arguments, float equality, identity-vs-equality confusion, shared mutable references, and float-constructed
Decimal- Python fits I/O-heavy automation and data plumbing; use vectorized libraries or other runtimes for CPU-bound hot paths
Glossary
Variable
- Name bound to an object reference.
- Variables in Python refer to objects rather than storing values directly.
- Reassigning
xchanges what the name points to; it does not mutate the previously referenced object.
Dynamic typing
- Types belong to objects at runtime rather than to variable names.
- The same name can be rebound to values of different types during execution.
- Without type hints and static analysis, type errors are detected only during execution.
int
- Arbitrary-precision integer type.
- Suitable for counters, indexes, and identifiers that exceed fixed-width integer limits.
- Python integers do not overflow or wrap, but very large values consume more memory and CPU.
float
- IEEE 754 double-precision floating-point type.
- Suitable for approximate numeric work where exact decimal semantics are not required.
- Binary floating-point cannot exactly represent values such as
0.1, so financial calculations should useDecimal.
Decimal
- Exact base-10 numeric type from
decimal.- Suitable for currency, tax, and fixed-point business rules; construct values from strings, not floats.
Decimal(0.1)preserves the float approximation, whereasDecimal("0.1")preserves the exact decimal value.
bool
- Logical type with values
TrueandFalse; subclass ofint.- Used in branching, filters, and truthiness checks.
- Falsy values include
0,0.0,0j,"",[],{},set(),None, andrange(0).
bytes/bytearray
- Immutable and mutable byte sequences.
- Used at text/binary boundaries, in file I/O, sockets, and encoded payloads.
- In Python 3,
strandbytesare distinct types and require explicit encode/decode at the boundary.
None
- Singleton representing absence of a value.
- Common in optional parameters, sentinel values, and functions with no explicit return.
- Use
is Noneinstead of== Nonebecause equality can be overridden by custom classes.
Mutable object
- Object whose state can change in place after creation.
- Shared references observe the same in-place changes.
- Assigning a mutable object to another name aliases the same object, so in-place changes are visible through both names.
Immutable object
- Object whose value cannot change after creation.
- Safe for shared state and commonly suitable for dict keys when hashing is stable.
- Immutability applies to the object itself, not to the variable holding the reference.
Hashable object
- Object with stable hash and equality semantics suitable for dict keys and set members.
- Required for dict keys and for membership in sets.
- Mutable containers such as
listanddictare not hashable and cannot be used as dict keys.
f-string
- Formatted string literal evaluated at runtime.
- Preferred interpolation form for new Python code.
- Omitting the
fprefix leaves the braces uninterpreted and silently produces a plain string.
Dunder method
- Special
__name__method invoked by operators and built-ins.- Defines custom arithmetic, iteration, representation, hashing, and protocol integration.
- If a class defines
__eq__, it must also be reviewed for compatible__hash__behavior in sets and dicts.
Walrus operator
:=
- Assignment expression that both binds and returns a value.
- Useful in read-and-test loops and compute-once filter patterns.
- It is valid only inside expressions and should not be treated as a replacement for standalone assignment.
EAFP
- Exception-first style: attempt the operation and catch expected failures.
- Common in parsing, file access, and dict lookups with narrow exception handling.
- Catch only the expected exception type; broad
except Exceptionblocks hide real defects.
Context manager
- Object implementing
__enter__and__exit__for scoped cleanup.- Used for files, locks, and temporary resources managed with
with.withguarantees cleanup even when the controlled block raises an exception.
Virtual environment
- Isolated package environment tied to a specific interpreter.
- Separates project dependencies from the system interpreter and from other projects.
- Install project dependencies into the active virtual environment rather than into the system interpreter.
GIL
- CPython lock that serializes Python bytecode execution across threads.
- Limits CPU-bound parallel execution in threaded Python code while still allowing useful I/O concurrency.
- Threading helps with I/O, but CPU-bound parallelism requires multiprocessing or native/vectorized code.
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()) # Machine3.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
ElysiumInspect 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\VAULTList 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.1Verify 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: MISSINGConsole 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 multiple values with separator and end parameters
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 | threeFormat 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.14Control 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-15a b c
a, b, c
abc
a → b → c
2024-3-15Control 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 filehello world!
loading...done!
Processing...
This goes to stderrEscape 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 escapedApply 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")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 inputHello, 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) # Got17
AlexVariables, 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 runtimePI = 3.14159
MAX_USERS = 100
API_URL = https://api.example.comNumeric 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 neededfloat — 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 digitsInspect 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 artifactinf
-inf
nan
0.30000000000000004Perform 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)) # magnitudez = (3+4j), type: <class 'complex'>
Real: 3.0, Imaginary: 4.0
(3-4j)
5.0Boolean, 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)) # Falsea = True, type: <class 'bool'>
b = False, type: <class 'bool'>
2
10
False
True
False
True
False
True
FalseConvert 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 idiomaticx = None, type: <class 'NoneType'>
True
TrueType 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=strImmutable 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=NoneTypeMutable 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=bytearraySpecialized 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.64Mutability 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? TrueOperators
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 = 1Comparison 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 equal10 == 20 : False
10 != 20 : True
10 > 20 : False
10 < 20 : True
10 >= 20 : False
10 <= 20 : TrueChain 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 : TrueIdentity, 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 : TrueTest 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 : FalseLogical 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': defaultTruthy/Falsy values — 0, 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)) = FalseBitwise 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 operatorsa = 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 evenSwap 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^y — x 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=5Assignment 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 → 256Compound 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 → 16Ternary, 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 → adultNested 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 itemsWalrus 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: line3Operator 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 operatorExponentiation 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
FalseMagic 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__— breaksdict/setusage- 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.yUse 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
1079245023883434373Access 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
FalseImplement 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 stoppedType 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__? FalseComplete 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__.pyInspect 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."""
passgreet
Returns a greeting message.
__main__
('Hello',)
__annotations__: {'name': <class 'str'>, 'greeting': <class 'str'>, 'return': <class 'str'>}
greetDefine a class hierarchy for attribute inspection
Setting up a simple Animal → Dog 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 = nameWalk 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
TrueMagic attribute quick reference
These attributes are present on all modules, classes, and functions and expose read-only metadata.
| Attribute | Purpose |
|---|---|
__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.
| Type | Example | Operational note |
|---|---|---|
int | 42 | Arbitrary precision integer |
float | 3.14 | IEEE 754 double-precision float |
complex | 3+4j | Built-in complex number |
bool | True | Subclass of int |
str | "hello" | Unicode text sequence |
bytes | b"hello" | Immutable byte sequence |
tuple | (1, 2, 3) | Ordered immutable container |
frozenset | frozenset({1, 2}) | Immutable set |
range | range(10) | Immutable arithmetic sequence |
None | None | Singleton null object |
Decimal | Decimal("3.14") | Exact base-10 numeric type |
Fraction | Fraction(1, 3) | Exact rational value |
namedtuple | Point(3, 4) | Tuple with named fields |
Mutable type reference
These mutable types need factories, copies, or coordination when shared across instances or threads.
| Type | Example | Safe 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) |
bytearray | bytearray(b"hi") | field(default_factory=bytearray) |
deque | deque([1, 2]) | field(default_factory=deque) |
defaultdict | defaultdict(int) | field(default_factory=lambda: defaultdict(int)) |
| User-defined mutable class | MyClass() | 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? FalseMutable 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? TrueDict 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=3Data 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.jsonI/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.054sRapid 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.0Constraints 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.015sMemory-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=4080Compile-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 strDesktop 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.6CPU 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.051sEngineering 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.50Use 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
TrueEnvironment 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
NoneAdd 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
NoneRuntime 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.1979Apply __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__"))FalsePrefer 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)42Pair __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)TrueCommon 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
TrueIdentity 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
TrueDecimal 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
TrueException-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
invalidPython 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")])okNone 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)NoneScope 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)1Local 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())3Interpreter 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))TruePackage 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
NoneProtected 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\.venvRelated Topics
Points to adjacent material when the core language types start affecting serialization and pipeline design decisions.
Adjacent references
- Data Architecture: Serialization — Serialization Formats for when
bytes, JSON, and Parquet choices matter in pipelines