PYTHON 3.12+ · 2026

Python Fundamentals:
From First Principles to Practical Patterns

A complete, densely-packed reference for Python 3.12+. Covers the full language from the ground up — dynamic typing, all built-in data structures, control flow, functions, object-oriented programming, modules, error handling, iterators, generators, decorators, type hints, and Pythonic best practices. Every section includes annotated code examples.

✍️ Glenn Junsay Pansensoy ⏱ 30 min read 📅 Mar 2026 🎓 Beginner → Intermediate 🐍 Python 3.12+

Advertisement

01 — Core Language

Core Language: Types, Variables & Scope

what is Python · dynamic & strong typing · variables · LEGB scope

What is Python?

Python is a high-level, interpreted, general-purpose language created by Guido van Rossum (1991). It emphasises readability — developers express ideas in fewer lines than Java or C++. Python runs cross-platform (Windows, macOS, Linux) and powers web backends, data science, AI/ML, automation, and scripting. Its motto: "batteries included" — a vast standard library ships with every install.

Dynamic & Strongly Typed

Python uses dynamic typing — types are inferred at runtime, no declarations needed. But it is strongly typed: mixing incompatible types (like "2" + 2) raises a TypeError instead of silently coercing. This gives prototyping speed without hidden bugs. Inspect types at runtime with type(x) or isinstance(x, int).
python · typing
# Type inspection x = 42 print(type(x)) # <class 'int'> print(isinstance(x, int)) # True print(isinstance(x, (int, float))) # True — multi-type check # print(x + " apples") # TypeError!

Variables & Assignment

Variables are created on first assignment. Python supports multiple assignment, tuple unpacking, and augmented assignment (+=, -=, *=, etc.). Names are case-sensitive and use snake_case by convention. Constants are written in ALL_CAPS (convention only — Python doesn't enforce immutability).
python · variables
name = "Alice" # str age = 30 # int pi = 3.14159 # float active = True # bool empty = None # NoneType a, b, c = 1, 2, 3 # tuple unpacking a, b = b, a # swap — no temp variable needed first, *rest = [1, 2, 3, 4] # extended unpacking score += 10 # augmented assignment MAX_RETRIES = 5 # constant by convention (ALL_CAPS)

Indentation & the LEGB Scope Rule

Blocks are defined by indentation (4 spaces — never mix tabs and spaces). No curly braces. Every indent level opens a new scope. Python resolves names using the LEGB rule: Local → Enclosing → Global → Built-in. Use global x to write to module scope from inside a function, and nonlocal x to write to an enclosing (non-global) scope — commonly used in closures.
python · scope & LEGB
x = "global" def outer(): x = "enclosing" def inner(): nonlocal x x = "local (modified enclosing)" print(x) # local (modified enclosing) inner() print(x) # local (modified enclosing) outer() print(x) # global (unchanged)

🔍 Key insight: Python's LEGB rule is why you can always read a global variable from inside a function, but need the global keyword to assign to it. Without global, the assignment creates a new local variable that shadows the global — a common source of subtle bugs.

Advertisement

02 — Data Types

Data Types: Numbers, Strings & Collections

int · float · complex · str · list · tuple · dict · set · bool

Numbers

Three numeric types: int (arbitrary precision — no overflow), float (IEEE 754 double), complex (3+4j). Key operators: // floor division, ** exponentiation, % modulo. The math module adds sqrt, log, ceil, floor, trig functions, and more. Use decimal.Decimal for exact decimal arithmetic (finance).
python · numbers
print(10 // 3) # 3 floor division print(10 % 3) # 1 modulo print(2 ** 10) # 1024 power print(abs(-7)) # 7 print(round(3.14159, 2)) # 3.14 import math print(math.sqrt(144)) # 12.0 print(math.pi) # 3.14159... print(math.log(100, 10)) # 2.0 (log base 10)

Strings

Strings are immutable sequences of Unicode characters. Use single, double, or triple quotes. f-strings (Python 3.6+) are the preferred interpolation method — they support full expressions inside {}. Strings support indexing, slicing, concatenation (+), and repetition (*). Key methods: .upper(), .lower(), .strip(), .split(), .join(), .replace(), .startswith(), .find(), .format().
python · strings
s = "Python" print(s[0]) # P (first char) print(s[-1]) # n (last char) print(s[1:4]) # yth (slice) print(s[::-1]) # nohtyP (reversed) print(" hi ".strip()) # "hi" print(",".join(["a","b","c"])) # a,b,c name, ver = "Glenn", 3.12 msg = f"Hello {name}, Python {ver:.1f}" print(msg) # Hello Glenn, Python 3.1 # Multiline string text = """Line one Line two Line three"""

Built-in Data Structures

Python's four core collections power almost every program. Choose the right one for the job — each has distinct performance characteristics and semantics.
📋 list

Ordered, mutable, allows duplicates. O(1) append and index access; O(n) insert/delete at arbitrary position. The go-to for sequences that need modification.

📌 tuple

Ordered, immutable. Slightly faster and more memory-efficient than lists. Use for fixed records, function return values, and as dictionary keys.

🗂 dict

Key→value mapping with O(1) average lookup, insert, and delete. Insertion-ordered since Python 3.7. Keys must be hashable (str, int, tuple).

🔵 set

Unordered, unique values only. O(1) membership test (in), union (|), intersection (&), difference (-). Perfect for deduplication and fast membership checks.

python · collections
# List fruits = ["apple", "mango", "guava"] fruits.append("lemon") fruits.sort() print(fruits[0]) # apple # Dict — creation, update, safe get person = {"name": "Glenn", "age": 25} person["city"] = "Manila" print(person.get("city", "unknown")) # Manila print(person.keys(), person.values()) # Set — dedup & intersection tags = {"python", "code", "python"} skills = {"python", "sql", "docker"} print(tags) # {'python', 'code'} print(tags & skills) # {'python'}
Use collections.defaultdict to avoid key-existence checks, collections.Counter for frequency counting, and collections.deque for efficient O(1) append/pop from both ends of a sequence.

03 — Control Flow

Control Flow: if, for, while & match

if · elif · else · for · while · break · continue · match (3.10+)

if / elif / else & Ternary

Conditional logic uses if, elif (not "else if"), and else. Python supports a compact ternary expression: x if condition else y. Since Python 3.10, the match statement adds powerful structural pattern matching — it goes far beyond simple switch-case, supporting sequence destructuring, class patterns, and guards.
python · conditionals
score = 85 if score >= 90: grade = "A" elif score >= 75: grade = "B" else: grade = "C" # Ternary status = "pass" if score >= 60 else "fail" print(grade, status) # B pass # match statement (Python 3.10+) def http_status(code): match code: case 200: return "OK" case 404: return "Not Found" case 500: return "Server Error" case _: return "Unknown"

for Loops

Python's for iterates over any iterable — lists, strings, dicts, ranges, generators. Use range(start, stop, step) for numeric sequences. enumerate() gives index + value together. zip() pairs two iterables in lockstep. break exits the loop early; continue skips to the next iteration. Loops can have an optional else clause that runs if the loop wasn't broken.
python · for loops
for i in range(5): print(i, end=" ") # 0 1 2 3 4 fruits = ["apple", "mango", "guava"] for i, fruit in enumerate(fruits, start=1): print(f"{i}: {fruit}") names = ["Ana", "Bob", "Chen"] scores = [95, 88, 92] for name, sc in zip(names, scores): print(f"{name}: {sc}") # for/else — "not found" pattern for x in [2, 4, 6]: if x % 2 != 0: break else: print("All even!") # runs — no break

while Loops

while repeats as long as a condition remains true. Use while True with a break for interactive loops. The optional else clause runs when the condition becomes false — but not if break was used. Always ensure the loop has a termination condition to avoid infinite loops.
python · while
n = 1 while n <= 5: print(n, end=" ") n += 1 # 1 2 3 4 5 # Walrus operator (:=) — assign & test in one step (3.8+) import random while (val := random.randint(1, 10)) != 7: print(f"Got {val}, trying again...") print("Found 7!")

Advertisement

04 — Functions

Functions: args, kwargs, Lambdas & Comprehensions

def · *args · **kwargs · lambda · map · filter · comprehensions

Defining & Calling Functions

Functions are defined with def. They support default parameters, keyword arguments, *args (variable positional), and **kwargs (variable keyword). Functions always return a value — omitting return returns None. Always write a docstring on the first line to document purpose, parameters, and return value.
python · functions
def power(base, exp=2): """Return base raised to exp (default: square).""" return base ** exp print(power(3)) # 9 print(power(2, 10)) # 1024 print(power(exp=3, base=4)) # 64 (keyword args) def summarise(*args, **kwargs): print("positional:", args) print("keyword:", kwargs) summarise(1, 2, name="Glenn", city="Manila")

Lambdas & Higher-Order Functions

Functions are first-class objects — assign them to variables, store in lists, pass as arguments, and return them from other functions. lambda creates a single-expression anonymous function. map(), filter(), and sorted(key=...) are the classic higher-order functions, though list comprehensions are usually preferred for readability.
python · lambdas & HOF
double = lambda x: x * 2 print(double(5)) # 10 nums = [3, 1, 4, 1, 5, 9] squares = list(map(lambda x: x**2, nums)) evens = list(filter(lambda x: x%2==0, nums)) print(squares) # [9, 1, 16, 1, 25, 81] print(evens) # [4] words = ["banana", "apple", "cherry"] print(sorted(words, key=lambda w: len(w))) # by length # Functions as arguments (passing behaviour) def apply(func, values): return [func(v) for v in values] print(apply(str.upper, ["hello", "world"]))

Comprehensions

Comprehensions are concise, readable, and faster than equivalent for-loops with .append(). Python supports list, dict, set, and generator comprehensions. Pattern: [expr for item in iterable if condition]. Generator comprehensions use () instead of [] and are lazy — they produce values on demand without building the full list in memory.
python · comprehensions
# List comprehension squares = [x**2 for x in range(10)] even_sq = [x**2 for x in range(10) if x%2==0] # Dict comprehension word_len = {w: len(w) for w in ["py", "code", "zen"]} # Set comprehension unique_lens = {len(w) for w in ["hi", "hey", "hello"]} # Generator expression (lazy — no list in memory) total = sum(x**2 for x in range(1_000_000)) # Nested comprehension (flatten a matrix) matrix = [[1,2],[3,4],[5,6]] flat = [n for row in matrix for n in row]

05 — Object-Oriented Programming

Object-Oriented Programming in Python

classes · __init__ · inheritance · super() · @property · __dunder__

Defining a Class

A class is a blueprint; an instance is a concrete object built from it. __init__ is the constructor — it runs whenever an instance is created. self refers to the instance. Define class attributes (shared by all instances) separately from instance attributes (unique per object). Always implement __repr__ for useful debugging output.
python · class basics
class Dog: species = "Canis familiaris" # class attribute (shared) def __init__(self, name: str, age: int): self.name = name # instance attributes self.age = age def bark(self) -> str: return f"{self.name} says Woof!" def __repr__(self) -> str: return f"Dog(name={self.name!r}, age={self.age})" def __str__(self) -> str: return f"{self.name}, {self.age} yrs" rex = Dog("Rex", 3) print(rex.bark()) # Rex says Woof! print(repr(rex)) # Dog(name='Rex', age=3) print(Dog.species) # Canis familiaris

Inheritance & Polymorphism

A child class inherits all attributes and methods from its parent. Call the parent constructor with super().__init__(). Override methods in the child to specialise behaviour — Python looks up the MRO (Method Resolution Order) to find the right method. Polymorphism lets different classes respond to the same method name in their own way, enabling flexible, extensible code.
python · inheritance
class Animal: def __init__(self, name: str): self.name = name def speak(self) -> str: return "..." class Dog(Animal): def speak(self) -> str: return f"{self.name}: Woof!" class Cat(Animal): def speak(self) -> str: return f"{self.name}: Meow!" # Polymorphism — same interface, different behaviour for animal in [Dog("Rex"), Cat("Luna")]: print(animal.speak()) print(Dog.__mro__) # (Dog, Animal, object)

Encapsulation & Properties

Use a single underscore (_name) to signal "internal use" (protected by convention), and double underscore (__name) to trigger name mangling (prevents accidental override in subclasses — not security). The @property decorator turns a method into a readable attribute; @attr.setter adds validated write access — the Pythonic way to implement getters/setters with invariant enforcement.
python · encapsulation
class BankAccount: def __init__(self, balance: float = 0): self.__balance = balance # private via name mangling @property def balance(self) -> float: return self.__balance # read-only attribute @balance.setter def balance(self, value: float): if value < 0: raise ValueError("Balance cannot be negative") self.__balance = value def deposit(self, amount: float): if amount <= 0: raise ValueError("Must be positive") self.__balance += amount acc = BankAccount(100) acc.deposit(50) print(acc.balance) # 150 # acc.balance = -10 # raises ValueError

🔍 Dunder methods (magic methods): Python's double-underscore methods let you define how objects behave with operators and built-in functions. Implement __len__ for len(obj), __eq__ for ==, __lt__ for <, __add__ for +, __iter__/__next__ for iteration, and __enter__/__exit__ for with statements. This is how Python's built-in types work under the hood.

Advertisement

06 — Modules & Packages

Modules, Packages & the Standard Library

import · from · __init__.py · stdlib · third-party packages · virtual envs

Every .py file is a module. Import with import module or from module import name. Packages are directories containing an __init__.py file. The standard library is vast — covering math, file I/O, networking, dates, JSON, CSV, regex, threading, and more — with no pip needed. Third-party packages are installed via pip from PyPI (Python Package Index), which hosts over 500,000 packages.
python · modules & stdlib
import math print(math.sqrt(144), math.pi) from datetime import date, timedelta today = date.today() next_week = today + timedelta(days=7) print(today, "→", next_week) import json data = {"name": "Glenn", "score": 99} json_str = json.dumps(data, indent=2) restored = json.loads(json_str) print(json_str) import re pattern = re.compile(r'\d{3}-\d{4}') print(pattern.findall("Call 123-4567 or 987-6543"))

Popular Third-Party Packages

Install with pip install <name>. Best practice: always use a virtual environment (python -m venv .venv) per project to isolate dependencies.

🔢 numpy / pandas

Fast numerical arrays and DataFrames for data analysis. The foundation of all Python data science work.

📊 matplotlib / seaborn

Publication-quality plotting and statistical visualisation. Seaborn wraps matplotlib with beautiful default styles.

🌐 requests / httpx

Elegant HTTP clients. requests is the classic sync option; httpx adds async support and HTTP/2.

⚡ fastapi / flask

Web frameworks. FastAPI for high-performance async APIs with automatic docs; Flask for lightweight control.

🗄️ sqlalchemy

The most powerful Python ORM. Maps database tables to Python classes and supports all major SQL databases.

🧪 pytest

The go-to testing framework. Fixtures, parametrize, plugins, and clean assertion introspection make testing productive.

07 — Error Handling & File I/O

Error Handling & File I/O

try · except · else · finally · raise · custom exceptions · open · pathlib

Exception Handling

try / except / else / finally manages error flow. Always catch specific exceptions — avoid bare except: which silently swallows every error including KeyboardInterrupt. else runs only if no exception occurred. finally always runs — perfect for resource cleanup. Raise custom errors by subclassing Exception and adding context.
python · exceptions
# Custom exception hierarchy class AppError(Exception): pass class ValidationError(AppError): def __init__(self, field, msg): super().__init__(f"{field}: {msg}") self.field = field def safe_divide(a, b): try: result = a / b except ZeroDivisionError: raise ValidationError("b", "cannot be zero") from None except TypeError as e: raise ValidationError("inputs", f"must be numeric: {e}") else: return result # runs only if no exception finally: print("safe_divide called") # always runs

File I/O & pathlib

Use open() to read and write files. Always use a with block (context manager) — it guarantees the file is closed even if an exception occurs. Mode flags: "r" read, "w" write (overwrites), "a" append, "rb"/"wb" binary. Use pathlib.Path for modern, readable, cross-platform path manipulation — it replaces os.path in all new code.
python · file I/O & pathlib
from pathlib import Path base = Path("data") base.mkdir(exist_ok=True) notes = base / "notes.txt" # Write with notes.open("w") as f: f.write("Python is amazing!\n") f.write("pathlib makes paths easy.\n") # Read all at once content = notes.read_text() print(content) # Read line by line (memory efficient) with notes.open() as f: for line in f: print(line.strip()) # Path operations print(notes.suffix) # .txt print(notes.stem) # notes print(notes.exists()) # True
Use json.dump(data, f) / json.load(f) for JSON files, csv.DictReader / csv.DictWriter for CSV, and pickle.dump / pickle.load for serialising Python objects (not for untrusted data).

08 — Advanced Patterns

Advanced Patterns: Generators, Decorators & Type Hints

yield · itertools · @wraps · lru_cache · type hints · dataclasses

Iterators & Generators

An iterator implements __iter__() and __next__(). A generator uses yield to produce values one at a time without building the full sequence in memory — it pauses execution at yield and resumes from that exact point on the next call to next(). This makes generators ideal for large datasets, infinite sequences, and streaming pipelines. Generator expressions are even more concise: sum(x**2 for x in range(n)).
python · generators
def countdown(n): while n > 0: yield n # suspends here, resumes on next() n -= 1 for num in countdown(5): print(num, end=" ") # 5 4 3 2 1 # Infinite Fibonacci — never runs out of memory def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b from itertools import islice print(list(islice(fibonacci(), 10))) # first 10 Fibonacci # Generator with send() — coroutine pattern def accumulator(): total = 0 while True: value = yield total if value is None: break total += value

Decorators

A decorator is a function that wraps another function, adding behaviour (logging, timing, caching, auth) without touching the original code. The @name syntax is syntactic sugar for func = decorator(func). Always apply @functools.wraps(func) inside your decorator to preserve the wrapped function's __name__ and __doc__ — critical for debugging and introspection tools.
python · decorators
import time from functools import wraps, lru_cache def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"{func.__name__} took {end-start:.4f}s") return result return wrapper @timer def slow_sum(n): return sum(range(n)) slow_sum(10_000_000) # slow_sum took 0.2134s # Built-in: memoisation with lru_cache @lru_cache(maxsize=None) def fib(n): return n if n < 2 else fib(n-1) + fib(n-2) print(fib(50)) # instant — results are cached

Type Hints 3.5+

Type hints annotate variables and function signatures with expected types. Python does not enforce them at runtime — they are for IDEs, type checkers (mypy, pyright), and self-documenting code. Python 3.9+ allows built-in generics: list[int], dict[str, Any]. Python 3.10+ uses X | Y union syntax. dataclasses (3.7+) combine type hints with automatic boilerplate generation.
python · type hints & dataclasses
from typing import Optional, Union from dataclasses import dataclass, field def greet(name: str, times: int = 1) -> str: return (f"Hello {name}! ") * times # Union type — int OR str (Python 3.10+ syntax) def parse(value: int | str) -> float: return float(value) # Optional (= T | None) def find_user(uid: int) -> Optional[str]: users = {1: "Glenn"} return users.get(uid) # Dataclass — auto __init__, __repr__, __eq__ @dataclass class Point: x: float y: float label: str = "origin" tags: list[str] = field(default_factory=list) p = Point(1.0, 2.0) print(p) # Point(x=1.0, y=2.0, label='origin', tags=[])

09 — Quick Reference

Operators Quick Reference

arithmetic · comparison · logical · identity · membership · bitwise · walrus

CategoryOperatorsExampleResult
Arithmetic+ - * / // % **2 ** 8256
Comparison== != < > <= >=3 < 5True
Logicaland or notx > 0 and x < 10bool
Identityis is notx is Nonebool
Membershipin not in"a" in "cat"True
Bitwise& | ^ ~ << >>0b1010 & 0b11008
Assignment= += -= *= /= //= %=x //= 3in-place
Walrus 3.8+:=if (n := len(a)) > 10assign & test
Dict merge 3.9+| |=a | bmerged dict
Type union 3.10+|int | strunion type
Python evaluates and / or with short-circuit evaluationa and b doesn't evaluate b if a is falsy. This is used idiomatically: name = user_input or "default".

Advertisement

10 — Best Practices

Pythonic Style & Best Practices

Zen of Python · PEP 8 · idioms · virtual envs · tooling

The Zen of Python

Run import this in any Python shell to read all 19 aphorisms. These are the guiding philosophy behind how Python code should be written. Every experienced Python developer has internalised them:
  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Readability counts.
  • Errors should never pass silently.
  • There should be one obvious way to do it.
  • If the implementation is hard to explain, it's a bad idea.

Pythonic Idioms

  • for x in items over for i in range(len(items))
  • f-strings over "Hello " + name or "%s" % name
  • with open(...) as f over manual f.open() / f.close()
  • List comprehensions over append() in a for-loop
  • x is None over x == None (identity, not equality)
  • if items: over if len(items) != 0:
  • Unpacking: first, *rest = [1, 2, 3, 4]
  • Dict merge 3.9+: merged = dict_a | dict_b
  • any() / all() over manual boolean loops
  • enumerate() over manual index counters
  • zip() to iterate two lists simultaneously

Tooling & Project Setup

shell · project setup
# Create isolated virtual environment python -m venv .venv source .venv/bin/activate # macOS/Linux # .venv\Scripts\activate # Windows # Check syntax without running python -m py_compile file.py # Format code (Black) pip install black black . # Static type checking (mypy) pip install mypy mypy main.py # Linting (ruff — ultra fast) pip install ruff ruff check . # Run tests (pytest) pip install pytest pytest -v
Use python -m pdb -c continue file.py to drop into the debugger when an exception occurs. In VS Code / PyCharm, set breakpoints visually. For quick inspections, breakpoint() (Python 3.7+) drops into pdb wherever you place it.

FAQ

Common Python Questions Answered

Python is dynamically and strongly typed. Dynamic means types are resolved at runtime — no declarations needed. Strong means Python won't silently coerce incompatible types; adding a string to an integer raises a TypeError. Python 3.5+ supports optional type hints for documentation and tools like mypy, but these are not enforced at runtime.

A list is mutable — you can add, remove, or change elements after creation. A tuple is immutable — once created, its contents cannot be changed. Tuples are slightly faster, use less memory, and can be used as dictionary keys. Use tuples for fixed records and function return values; lists for ordered sequences that need modification.

A decorator is a function that wraps another function to add behaviour without modifying the original. The @syntax is shorthand for func = decorator(func). Use decorators for cross-cutting concerns: logging, timing, caching (@lru_cache), authentication, retry logic, and input validation. The standard library uses them heavily: @property, @staticmethod, @classmethod, @functools.wraps.

A generator uses yield to produce values one at a time instead of building the entire sequence in memory. When execution hits yield, the function pauses and returns the value — resuming from that exact point on the next call to next(). This makes generators ideal for large files, infinite sequences, and streaming pipelines where loading everything at once would exhaust memory.

LEGB is Python's variable name resolution order: Local → Enclosing → Global → Built-in. Python searches each scope until the name is found or a NameError is raised. Use global to write to module-level scope from inside a function, and nonlocal to write to an enclosing (non-global) scope in a closure.

Use @dataclass whenever your class is primarily a container for data fields. Dataclasses automatically generate __init__, __repr__, __eq__, and optionally __hash__, __lt__ (with order=True), and frozen immutability (with frozen=True). They significantly reduce boilerplate for simple value objects, configuration holders, and DTOs (Data Transfer Objects). For classes with significant behaviour/logic, a regular class is often clearer.

Share This Article

Click a platform above to generate an AI-crafted caption for that network.

Community

Comments

0 comments

Be the first to comment! 👋

Stay in the Loop

Get new Python tutorials, deep-dives, and coding guides delivered to your inbox.

No spam Free forever Unsubscribe anytime

Join readers learning Python from first principles.