Introduction
Why do some programming languages feel natural while others are frustrating? Python’s popularity stems from its emphasis on readability and simplicity, which make coding more intuitive.
Python is a high-level, interpreted language created by Guido van Rossum in 1991. It emphasizes clear syntax and design to reduce cognitive load.
Understanding Python fundamentals matters because the language’s design shapes how you solve problems. Knowing why Python works the way it does helps you write code that others can maintain, debug efficiently, and avoid common pitfalls that cause production bugs.
What this is (and isn’t): This article explains Python’s core principles and design decisions, focusing on why Python works and how fundamental concepts fit together. It doesn’t teach you to write your first program or cover advanced topics like metaclasses or async programming.
Why Python fundamentals matter:
- Readable code - Python’s syntax reduces boilerplate, simplifying code for easier understanding later.
- Faster debugging - Understanding Python’s execution model aids quick issue diagnosis.
- Better design choices - Knowing built-in data structures avoids reinventing the wheel.
- Team productivity - Shared understanding of Python idioms enhances code reviews and collaboration.
This article outlines core concepts every Python developer encounters:
- Python’s execution model – how Python runs your code
- Data types and structures – built-in types and when to use them
- Functions and scope – how code reuse works
- Object-oriented programming – classes, inheritance, and composition
- Error handling – exceptions and debugging
- Modules and packages – code organization
- Common patterns – Python idioms and best practices

Type: Explanation (understanding-oriented). Primary audience: beginner to intermediate developers learning Python or transitioning from other languages
Prerequisites & Audience
Prerequisites: Basic programming knowledge, including variables, loops, and conditionals in at least one language. If new to programming, start with a tutorial before returning to grasp the “why” behind Python’s design.
Primary audience: Developers with Python experience or transitioning from Java, JavaScript, or C++ need to understand Python’s mental model.
Jump to: Execution Model • Data Types • Functions • OOP • Errors • Modules • Patterns • Mistakes • Glossary
If you’re brand new to Python, read Section 1 first to understand the execution model, then jump to Section 2 for data types. If you’re coming from another language, start with Section 4 to see how Python’s object model differs from what you know.
Escape routes: If you need practical examples, read Section 7 on Python idioms, then return to earlier sections for context.
TL;DR – Python Fundamentals in One Pass
If you only remember one thing, make it this:
- Everything is an object, so integers, functions, and classes all have types and methods
- Indentation matters, Python uses whitespace instead of braces
- Duck typing wins, if it acts like a list, treat it as one
- Explicit beats implicit, Python favors clear, not clever, code
The Python Workflow:
- Write clear code
- Test with real data
- Debug with stack traces
- Refactor for readability
Learning Outcomes
By the end of this article, you will be able to:
- Explain why Python is interpreted and how that affects performance and debugging.
- Explain why Python uses duck typing over static types and when to add type hints.
- Explain why mutable default arguments cause bugs and how to avoid them.
- Learn how Python’s data structures work and when to use lists, dictionaries, or sets.
- Explain how scope and closures influence variable access and function behavior.
- Explain how Python’s object model differs from Java or C++, and when to prefer composition over inheritance.
Section 1: Python’s Execution Model – How Python Runs Code
Python is an interpreted language, meaning your source code is compiled to bytecode and executed by the Python interpreter rather than compiled to machine code. This influences how you develop, debug, and deploy Python programs.
Interpretation versus Compilation
When you run a Python file, the interpreter reads your source code, converts it to bytecode in __pycache__ directories, and executes it in the Python Virtual Machine (PVM).
This differs from compiled languages like C or Rust, where source code is compiled into machine code before execution. The trade-off is startup time and raw performance for faster development cycles and cross-platform compatibility.
Why this matters: Understanding interpretation helps explain why:
- Python programs start quickly but run slower than compiled ones.
- You can run the same Python code on Windows, Linux, and macOS without recompiling.
- Syntax errors occur at runtime, not during compile time.
- You can use the interactive REPL to test code snippets immediately
Dynamic Typing and Duck Typing
Python is dynamically typed. Variables don’t have declared types, and the interpreter checks types at runtime when operations execute.
x = 42 # x is an integer
x = "hello" # now x is a string
x = [1, 2, 3] # now x is a listThis flexibility comes with responsibility. Type errors appear only when code runs, not when written.
Python uses duck typing: “If it walks like a duck and quacks like a duck, it’s a duck.” The interpreter focuses on methods and attributes, not class hierarchy.
def double_length(thing):
return len(thing) * 2
double_length("hello") # Works: strings have len()
double_length([1, 2, 3]) # Works: lists have len()
double_length(42) # Fails at runtime: integers don't have len()Why this matters: Duck typing makes Python code flexible and concise, but needs defensive programming or type hints to catch errors early. Python 3.5+ supports optional type hints that tools like mypy can check without affecting runtime.
Everything is an Object
In Python, everything is an object with a type, including integers, functions, and classes, simplifying the mental model.
x = 42
print(type(x)) # <class 'int'>
print(x.bit_length()) # Method on an integer: 6
def greet():
return "hello"
print(type(greet)) # <class 'function'>
print(greet.__name__) # Attribute on a function: 'greet'Numbers have methods, functions have attributes, and classes are objects.
Why this matters: This consistency allows passing functions as arguments, storing classes in lists, and introspecting objects at runtime. It also means that overhead makes Python integers slower than C integers, but more flexible.
Quick Check: Execution Model
Before moving on, test your understanding:
- What happens when you run a
.pyfile? - Why can the same variable hold an integer and a string?
- What does “duck typing” mean in practice?
Answer guidance: Ideal result: You can explain that Python converts source code to bytecode, runs it in a VM, variables can reference any object type, and duck typing checks for methods rather than the class hierarchy.
If any answer feels unclear, reread the relevant section and try explaining it out loud.
Section 2: Data Types and Structures – Choosing the Right Tool
Python offers built-in data types that efficiently solve problems. Choosing the right one prevents bugs and performance issues.
Basic Types
Integers and floats represent numbers. Integers have unlimited precision (Python allocates more memory as needed), while floats are 64-bit IEEE 754 double-precision floating-point numbers.
big_number = 123456789012345678901234567890 # No overflow
pi = 3.14159 # FloatStrings are immutable sequences of Unicode characters. Immutability means you can’t change a string in place; you create a new one.
s = "hello"
s[0] = "H" # Error: strings are immutable
s = "H" + s[1:] # Creates new string: "Hello"Booleans are True and False, which are actually integers (1 and 0) under the hood.
Collections: Lists, Tuples, Dictionaries, and Sets
Lists are mutable, ordered sequences used when you need to add, remove, or change elements.
fruits = ["apple", "banana", "cherry"]
fruits.append("date")
fruits[1] = "blueberry"Lists are implemented as dynamic arrays, giving O(1) access by index and O(1) amortized append, but O(n) insert in the middle.
Tuples are immutable, ordered sequences used for fixed collections that shouldn’t change.
coordinates = (10, 20)
# coordinates[0] = 15 # Error: tuples are immutableTuples are faster, use less memory than lists, and are hashable, so they can be dictionary keys.
Dictionaries are mutable key-value maps used for lookups by key, implemented as hash tables.
person = {"name": "Alice", "age": 30}
print(person["name"]) # Fast O(1) lookup: "Alice"
person["city"] = "Seattle"Dictionaries keep insertion order from Python 3.7. Keys must be hashable, like strings, numbers, or tuples.
Sets are mutable collections of unique elements used to remove duplicates or check membership quickly.
unique_numbers = {1, 2, 3, 2, 1} # {1, 2, 3}
print(2 in unique_numbers) # Fast O(1): TrueSets are implemented as hash tables without values.
When to Use Each Structure
Mutability and Aliasing
Mutable objects like lists can be modified, causing aliasing bugs when multiple variables reference the same object.
a = [1, 2, 3]
b = a # b and a point to the same list
b.append(4)
print(a) # [1, 2, 3, 4] - changed!Use slicing or the copy module to copy instead of alias.
b = a[:] # Shallow copy
b = a.copy() # Also shallow copyWhy this matters: Aliasing bugs occur in production when modifying shared data structures unexpectedly, but immutable types like strings, tuples, and numbers don’t have this issue.
Quick Check: Data Structures
Before moving on, test your understanding:
- When should you use a tuple instead of a list?
- Why can’t you use a list as a dictionary key?
- What occurs when two variables reference the same list and it’s modified?
Answer guidance: Ideal result: Tuples are for immutable, hashable sequences and can be dictionary keys. Lists are mutable, unhashable. Aliasing causes changes to affect all references.
If unclear, reread the mutability section and try creating examples in a Python REPL.
Section 3: Functions and Scope – Code Reuse Done Right
Functions are first-class objects in Python, allowing passing, returning, and storing them.
Defining Functions
Functions use def, default parameters, and return to send values.
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice")) # "Hello, Alice!"
print(greet("Bob", greeting="Hi")) # "Hi, Bob!"Python supports positional and keyword arguments, which clarify code when calling functions with many parameters.
Scope and the LEGB Rule
Python resolves variable names using the LEGB rule:
- Local: variables defined in the current function
- Enclosing: variables in outer functions (closures)
- Global: variables at module level
- Built-in: Python’s built-in names like
len,print
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"Why this matters: Understanding scope prevents bugs like shadowing variables or expecting a function to modify globals but creating locals instead.
Closures
A closure is a function that retains variables from its enclosing scope even after exit.
def make_multiplier(n):
def multiply(x):
return x * n # n is from enclosing scope
return multiply
times_three = make_multiplier(3)
print(times_three(10)) # 30Closures enable patterns like decorators and factory functions.
Mutable Default Arguments (The Classic Trap)
Don’t use mutable objects as default arguments because the default is created once at function definition, not on each call.
# WRONG
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] - unexpected!
# CORRECT
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_to(1)) # [1]
print(append_to(2)) # [2] - correct!Why this matters: This common Python bug helps prevent mysterious state leaks between function calls.
Quick Check: Functions and Scope
Before moving on, test your understanding:
- What’s the difference between positional and keyword arguments?
- Why does a closure remember outer scope variables?
- Why should you avoid mutable default arguments?
Answer guidance: Ideal result: Positional arguments use position, keyword arguments use names. Closures capture enclosing scope variables. Mutable defaults are created once and shared across calls.
If unclear, copy the mutable default example into a REPL and observe the behavior.
Section 4: Object-Oriented Programming – Classes and Objects
Python supports object-oriented programming with classes, inheritance, and special methods that customize object behavior.
Classes and Instances
A class is a template for objects; instances are the objects created from it.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
return f"{self.name} says woof!"
buddy = Dog("Buddy", 5)
print(buddy.bark()) # "Buddy says woof!"The __init__ method initializes new instances. self refers to the current instance and must be the first parameter of instance methods.
Attributes and Methods
Attributes are variables attached to objects. Methods are functions attached to objects.
buddy.name # Instance attribute
buddy.bark() # Instance methodPython also supports class attributes shared by all instances:
class Dog:
species = "Canis familiaris" # Class attribute
def __init__(self, name):
self.name = name # Instance attributeWhy this matters: Instance attributes are unique to each object, while class attributes are shared. Changing a class attribute impacts all instances unless an instance attribute shadows it.
Inheritance and Composition
Inheritance creates a new class from an existing one.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement")
class Dog(Animal):
def speak(self):
return f"{self.name} says woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says meow!"Composition involves creating classes from other objects instead of inheritance.
class Engine:
def start(self):
return "Engine started"
class Car:
def __init__(self):
self.engine = Engine() # Composition
def start(self):
return self.engine.start()Prefer composition over inheritance in most cases. Inheritance causes tight coupling, while composition keeps classes independent.
Special Methods (Dunder Methods)
Special methods like __init__, __str__, and __len__ customize object behavior.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # "Point(1, 2)" uses __str__
p3 = p1 + p2 # Point(4, 6) uses __add__Common special methods include:
__init__: initialize instances__str__: human-readable string representation__repr__: developer-friendly representation__len__: supportlen()function__getitem__: support indexing likeobj[key]__add__: support+operator
Quick Check: Object-Oriented Programming
Before moving on, test your understanding:
- What distinguishes a class attribute from an instance attribute?
- When should you use composition over inheritance?
- What does the
__init__method do?
Answer guidance: Ideal result: Class attributes are shared; instance attributes are per-object. Use composition for object relationships, inheritance for shared behavior. __init__ initializes new instances.
If unclear, create a small class hierarchy and experiment with attributes.
Section 5: Error Handling – Exceptions and Debugging
Python uses exceptions to signal errors. Understanding exception handling helps write robust code and debug issues efficiently.
How Exceptions Work
When Python encounters an error, it raises an exception and stops execution unless you handle it.
def divide(a, b):
return a / b
divide(10, 0) # ZeroDivisionError: division by zeroUse try/except to handle expected errors:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(safe_divide(10, 0)) # None
print(safe_divide(10, 2)) # 5.0Exception Hierarchy
Python’s exceptions form a hierarchy with BaseException at the root. Most errors inherit from Exception.
Common exceptions:
ValueError: invalid value (e.g.,int("abc"))TypeError: wrong type (e.g.,"hello" + 5)KeyError: missing dictionary keyIndexError: list index out of rangeFileNotFoundError: file doesn’t exist
Catch specific exceptions, not bare except:
# WRONG
try:
data = load_data()
except: # Catches everything, including KeyboardInterrupt
data = None
# CORRECT
try:
data = load_data()
except (FileNotFoundError, ValueError) as e:
print(f"Error loading data: {e}")
data = NoneStack Traces
When an exception isn’t caught, Python prints a stack trace showing the call stack.
def a():
b()
def b():
c()
def c():
raise ValueError("Something went wrong")
a() # Prints full stack trace: a() -> b() -> c()Stack traces read bottom-to-top; the error is at the bottom with the call path above.
Why this matters: Stack traces are your main debugging tool. Learn to read them efficiently to diagnose issues quickly.
Quick Check: Error Handling
Before moving on, test your understanding:
- Why should you catch specific exceptions instead of bare
except? - How do you read a Python stack trace?
- When should you let exceptions propagate instead of catching them?
Answer guidance: Ideal result: Specific exceptions prevent catching system signals. Stack traces show the call path to the error. Let exceptions propagate when the current function can’t handle them meaningfully.
If unclear, write code that raises exceptions and practice reading the traces.
Section 6: Modules and Packages – Organizing Code
Python organizes code into modules (single .py files) and packages (directories with __init__.py).
Importing Modules
Use import to load code from other modules:
import math
print(math.sqrt(16)) # 4.0
from math import sqrt
print(sqrt(16)) # 4.0
from math import sqrt as square_root
print(square_root(16)) # 4.0Avoid from module import * because it pollutes the namespace and makes dependencies unclear.
Creating Modules
Any .py file is a module. If you have utils.py:
# utils.py
def greet(name):
return f"Hello, {name}!"You can import it:
# main.py
from utils import greet
print(greet("Alice"))Packages
A package is a directory with an __init__.py file:
mypackage/
__init__.py
module1.py
module2.pyImport from packages:
from mypackage import module1
from mypackage.module2 import some_functionThe if __name__ == "__main__": Pattern
This pattern lets modules be both importable and executable:
# utils.py
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
# Only runs when executed directly, not when imported
print(greet("Alice"))Why this matters: This pattern lets you write modules with test code that doesn’t run when imported elsewhere.
Quick Check: Modules and Packages
Before moving on, test your understanding:
- What’s the difference between a module and a package?
- Why should you avoid
from module import *? - What does
if __name__ == "__main__":do?
Answer guidance: Ideal result: Modules are files, packages are directories with __init__.py. Star imports hide dependencies. The name check prevents code from running on import.
If unclear, create a small package and experiment with imports.
Section 7: Python Idioms – Common Patterns
Python has idioms that improve code readability and efficiency.
List Comprehensions
List comprehensions create lists concisely:
# Traditional loop
squares = []
for x in range(10):
squares.append(x ** 2)
# List comprehension
squares = [x ** 2 for x in range(10)]
# With condition
even_squares = [x ** 2 for x in range(10) if x % 2 == 0]Dictionary and set comprehensions work similarly:
square_dict = {x: x ** 2 for x in range(5)}
unique_lengths = {len(word) for word in ["hello", "world", "hi"]}Iteration and Generators
Python’s for loop iterates over any iterable like lists, tuples, dictionaries strings:
for char in "hello":
print(char)
for key, value in {"a": 1, "b": 2}.items():
print(key, value)Generators lazily produce values, saving memory:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Only computes values as needed
for num in fibonacci():
if num > 100:
break
print(num)Context Managers
Context managers handle setup and teardown automatically:
# Automatically closes the file
with open("data.txt") as f:
content = f.read()
# File is closed here, even if an exception occurredCreate custom context managers with __enter__ and __exit__ methods.
Unpacking
Python supports unpacking sequences:
a, b = [1, 2]
x, y, z = "abc"
# Swap variables
a, b = b, a
# Extended unpacking
first, *middle, last = [1, 2, 3, 4, 5]
# first=1, middle=[2, 3, 4], last=5Quick Check: Python Idioms
Before moving on, test your understanding:
- When should you use list comprehension instead of a loop?
- What’s the benefit of generators over lists?
- Why use context managers for file operations?
Answer guidance: Ideal result: List comprehensions are clearer for simple transformations. Generators save memory for large sequences. Context managers ensure cleanup occurs even in the face of exceptions.
If unclear, try writing the same code both ways and compare readability.
Section 8: Common Python Mistakes – What to Avoid
Understanding common mistakes helps you avoid complex debugging sessions and production bugs.
Mistake 1: Mutable Default Arguments
We covered this earlier, but it’s common enough to repeat.
Incorrect:
def add_item(item, items=[]):
items.append(item)
return itemsCorrect:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsMistake 2: Modifying a List While Iterating
Modifying a list during iteration causes skipped elements or errors.
Incorrect:
numbers = [1, 2, 3, 4, 5]
for n in numbers:
if n % 2 == 0:
numbers.remove(n) # Modifies list during iterationCorrect:
numbers = [1, 2, 3, 4, 5]
numbers = [n for n in numbers if n % 2 != 0]Mistake 3: Using is Instead of ==
is checks object identity (same object in memory), == checks value equality.
Incorrect:
a = [1, 2, 3]
b = [1, 2, 3]
print(a is b) # False - different objectsCorrect:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True - same valuesUse is only for None checks:
if value is None:
# ...Mistake 4: Catching Too Broad Exceptions
Bare except blocks catch signals, complicating debugging.
Incorrect:
try:
risky_operation()
except: # Catches everything
passCorrect:
try:
risky_operation()
except (ValueError, KeyError) as e:
logger.error(f"Operation failed: {e}")Mistake 5: Not Closing Resources
Always close files, network, and database connections.
Incorrect:
f = open("data.txt")
data = f.read()
# File never closedCorrect:
with open("data.txt") as f:
data = f.read()
# Automatically closedQuick Check: Common Mistakes
Test your understanding:
- Why do mutable default arguments cause bugs?
- When should you use
isversus==? - Why are bare
exceptclauses problematic?
Answer guidance: Ideal result: Mutable defaults are shared across calls. is checks identity, == checks value. Bare except catches system signals and hides bugs.
If issues are found, revisit the specific examples of mistakes.
Common Misconceptions
Common misconceptions about Python include:
“Python is slow, so it’s not suitable for real applications.” Python’s interpreter has overhead, but most slowness in production arises from algorithms and I/O, not speed. Libraries like NumPy use C for critical operations. Major systems like Instagram, Dropbox, and YouTube run on Python.
“Indentation-based syntax causes bugs.” Indentation enforces structure like braces, ensuring consistency. Switching to spaces eliminated mixed tab/space bugs. Most indentation issues are actually logic errors present in any syntax.
“Dynamic typing means you can’t build large systems.” Type hints (PEP 484) offer optional static checking while maintaining runtime flexibility. Tools like mypy detect errors during development. Many Python projects lack type hints, but they aid large codebases.
“You need classes for everything in Python.” Python supports multiple paradigms; functions, dictionaries, and modules often solve problems more simply than classes. Use classes when shared state and behavior are needed, not by default.
“Global variables are always bad in Python.” Module-level constants and configuration are fine. The problem is mutable global state that functions modify unexpectedly. Immutable module-level values are idiomatic Python.
When NOT to Use Python
Python isn’t always the right choice. Understanding when to skip it helps you focus effort where it matters.
Systems programming or embedded systems - Python’s interpreter overhead and memory use make it impractical for resource-constrained environments; use C, C++, or Rust instead.
Real-time processing with demanding latency requirements - Garbage collection pauses and interpreter overhead hinder sub-millisecond latency. Use Go, Rust, or Java for low-latency systems.
CPU-intensive numerical computing (without libraries) - Pure Python loops are slow for heavy number crunching. Use NumPy, Pandas, or write performance-critical sections in C/Cython. Or use Julia or Fortran for numerical work.
Mobile app development - Python has limited support for iOS and Android; use Swift/Kotlin or cross-platform frameworks like React Native or Flutter.
Performance-critical games or graphics - Python’s overhead makes it unsuitable for AAA games or real-time graphics. Use C++ with engines like Unity or Unreal. Python works for game scripting and tools.
Even when skipping Python for core functionality, it’s useful for build tools, testing, and automation.
Building with Python
Python fundamentals form a foundation for maintainable, debuggable code.
Core Principles
- Understand the execution model - Python’s interpreted, dynamically-typed nature impacts performance, debugging, and workflow.
- Choose the proper data structure - Lists, tuples, dictionaries, and sets each have specific performance characteristics and use cases.
- Master functions and scope - First-class functions, closures, and the LEGB rule enable powerful patterns but require understanding to avoid bugs.
- Use objects when appropriate - Classes and inheritance solve some problems, but composition and plain functions often work better.
- Handle errors explicitly - Exceptions offer error handling and debugging via stack traces.
How These Concepts Connect
Python’s execution model explains how dynamic typing, duck typing, data structures, functions, objects, and error handling work together to build robust systems.
These concepts build on each other: understanding execution improves debugging, knowledge of data structures helps select functions’ parameters and return types, and mastering functions naturally leads to understanding classes as bundles of related functions and data.
Getting Started with Python
If you’re new to Python, start with a narrow, repeatable workflow:
- Install Python and verify it works with
python3 --version - Write simple scripts that solve one problem at a time
- Use the REPL to test snippets and explore objects
- Read error messages carefully and learn to interpret stack traces
- Study existing code in libraries or open source projects
Once this feels routine, expand to larger projects with multiple modules and tests.
Next Steps
Immediate actions:
- Install Python 3.10+ and run
python3to open the REPL. - Write a script that reads a file, processes data, and writes results.
- Practice with list comprehensions and dictionary operations.
Learning path:
- Read The Python Tutorial for hands-on practice.
- Study Effective Python for intermediate patterns.
- Explore the Python Standard Library to see what’s built in.
Practice exercises:
- Implement a simple data parser (CSV, JSON) using built-in modules.
- Write a class hierarchy for a small domain, like shapes with area calculations.
- Create a module with functions and test it with doctests or pytest.
Questions for reflection:
- When should I use a list, a set, or a dictionary?
- How does Python’s scope affect closures and nested functions?
- What makes code “Pythonic” versus just working?
The Python Workflow: A Quick Reminder
Before we conclude, here’s the core workflow one more time:
- Write clear code
- Test with real data
- Debug with stack traces
- Refactor for readability
Python values clarity over cleverness. Write code understandable six months from now.
Final Quick Check
Before you move on, see if you can answer these out loud:
- How does Python execute your code?
- When should you use a tuple instead of a list?
- Why are mutable default arguments problematic?
- What’s the difference between
isand==? - How do you handle exceptions properly?
If any answer feels fuzzy, revisit the matching section and skim the examples again.
Self-Assessment – Can You Explain These in Your Own Words?
Before moving on, see if you can explain these concepts in your own words:
- Why Python uses duck typing and what that means for your code
- How the LEGB scope rule affects variable lookup
- When to use composition instead of inheritance
If you can explain these clearly, you’ve internalized the fundamentals.
Glossary
Duck typing: A typing system that assesses an object’s suitability based on methods and properties, not its class or inheritance.
Bytecode: An intermediate Python code representation the Python Virtual Machine executes.
LEGB rule: The order Python searches for variables: Local, Enclosing function, Global module, Built-in.
Closure: A function that remembers variables from its scope even after exit.
Mutable: Objects that can be modified after creation (lists, dictionaries, sets).
Immutable: Objects that cannot be modified after creation (strings, tuples, numbers).
Aliasing: When multiple variables reference the same object in memory.
First-class function: Functions that can be passed as arguments, returned from other functions, and assigned to variables.
Special methods: Methods with double underscores (dunder methods) that customize object behavior, like __init__ or __str__.
Context manager: An object that defines __enter__ and __exit__ methods for resource management, typically used with the with statement.
Generator: A function using yield to lazily produce values one at a time instead of computing all at once.
Iterable: Any object that can be looped over (lists, tuples, strings, dictionaries, generators).
Type hints: Optional annotations that specify expected types for variables, parameters, and return values (PEP 484).
REPL: Read-Eval-Print Loop, an interactive environment where you can type Python expressions and see results immediately.
References
Official Python Documentation
- The Python Tutorial: Official introduction to Python concepts and syntax.
- Python Standard Library: Documentation for built-in modules and functions.
- Python Language Reference: Detailed specification of Python syntax and semantics.
Books and Learning Resources
- Effective Python by Brett Slatkin: 90 specific ways to write better Python code.
- Fluent Python by Luciano Ramalho: Deep dive into Python’s data model and idioms.
- Python Cookbook by David Beazley and Brian K. Jones: Recipes for common Python tasks.
Language Design and History
- PEP 20: The Zen of Python: Guiding principles for Python design.
- PEP 8: Style Guide for Python Code: Official style conventions.
- The History of Python: Guido van Rossum’s blog about Python’s evolution.
Type Hints and Static Analysis
- PEP 484: Type Hints: Specification for type annotation syntax.
- mypy: Static type checker for Python.
- Type Hints Cheat Sheet: Quick reference for type annotation syntax.
Note on Verification
Python evolves with new releases every year. Verify syntax and library details match your Python version. The Python documentation includes version-specific notes for compatibility.
Comments #