I. Functions - The Core Building Blocks
Defining and Calling Functions
-
Syntax:
def function_name(parameters):followed by an indented block. -
returnStatement: Exits the function and passes back a value. Can return multiple values as a tuple (e.g.,return x, y).def square(x): return x * x
Parameters and Arguments
| Concept | Description | Example |
|---|---|---|
| Positional Arguments | Must match the order of parameters in the function definition. | greet("Alice", "Hello") |
Keyword Arguments (kwargs) |
Passed by parameter name; order doesn't matter. | greet(name="Bob", msg="Hi") |
| Default Values | Parameters can have defaults; used if argument is omitted. | def greet(name, msg="Hello"): |
*args |
Captures extra positional arguments as a tuple. | def func(a, *args): |
**kwargs |
Captures extra keyword arguments as a dict. | def func(a, **kwargs): |
Variable Scope and Namespaces (LEGB Rule)
The LEGB Rule defines the scope resolution order:
-
Local: Inside the current function.
-
Enclosing: In any enclosing function (for nested functions).
-
Global: At the top level of the module/file.
-
Built-in: In Python's built-in names (e.g.,
len,print).
-
globalkeyword: Modifies a variable in the Global scope from within a local scope. -
nonlocalkeyword: Modifies a variable in the Enclosing (non-local) scope from within a nested function.
Function Objects and First-Class Citizenship
Functions in Python are first-class objects:
-
Can be assigned to variables:
my_func = greet -
Can be passed as arguments (Higher-Order Functions):
def call_twice(func, x): return func(func(x)) -
Can be returned from other functions (Closures): A nested function remembering values from its enclosing scope.
def make_multiplier(n): def multiplier(x): return x * n # 'n' is remembered from enclosing scope return multiplier
Lambda Expressions
-
Syntax:
lambda arguments: expression -
Used for small, anonymous functions, often with
map(),filter(),sorted(). -
Limitation: Can only contain a single expression (no statements like
if-elseblocks,for,while). Use regulardeffor complex logic.squares = list(map(lambda x: x**2, [1,2,3]))
Docstrings and Annotations
-
Docstring: A string literal as the first statement in a function/module/class. Accessed via
function.__doc__. Crucial for documentation.def add(a, b): """Return the sum of two numbers.""" return a + b -
Annotations (Type Hints): Optional metadata about parameter/return types.
def greet(name: str) -> str: return "Hello " + name # Access via: greet.__annotations__ -> {'name': <class 'str'>, 'return': <class 'str'>}
[!TIP] Exam Focus: Be prepared to trace scope in nested functions using LEGB. Understand the difference between
*args(tuple) and**kwargs(dict). Lambda is for simple, one-line functions.
II. Core Data Structures - Lists, Tuples, Dictionaries, Sets
Lists (Mutable Sequences)
| Operation | Time Complexity (Average) | Description |
|---|---|---|
append(item) |
$O(1)$ | Add item to end. |
insert(i, item) |
$O(n)$ | Insert at index i (shifts elements). |
pop() / pop(-1) |
$O(1)$ | Remove/return last item. |
pop(i) |
$O(n)$ | Remove/return item at index i. |
remove(item) |
$O(n)$ | Remove first occurrence of item. |
index(item) |
$O(n)$ | Return first index of item. |
in (membership) |
$O(n)$ | Check if item exists. |
sort() |
$O(n \log n)$ | In-place sort (Timsort). |
Slicing [i:j:k] |
$O(k)$ | Creates a new list of size k. |
-
List Comprehension:
[expression for item in iterable if condition]squares = [x**2 for x in range(10) if x % 2 == 0] -
Shallow vs. Deep Copy:
-
Shallow: New outer list, but inner objects are references.
new_list = old_list[:]orold_list.copy(). -
Deep: Recursively copies all nested objects. Use
import copy; new_list = copy.deepcopy(old_list).
-
Tuples (Immutable Sequences)
-
Creation:
t = (1, 2, 3)ort = 1, 2, 3(comma creates tuple). Single-element tuple:t = (1,). -
Immutability: Cannot change, add, or remove elements after creation. Hashable if all elements are hashable → can be dictionary keys.
-
Packing/Unpacking:
a, b, c = (1, 2, 3)ort = 1, 2, 3. -
namedtuple: Factory function for creating tuple subclasses with named fields.from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(11, y=22)
Dictionaries (Key-Value Mappings)
| Operation | Average Time Complexity |
|---|---|
d[key] / d.get(key) |
$O(1)$ |
d[key] = value |
$O(1)$ |
key in d |
$O(1)$ |
del d[key] |
$O(1)$ |
keys(), values(), items() |
$O(1)$ (return view objects, not lists) |
-
Dictionary Comprehension:
{key_expr: value_expr for item in iterable}{x: x**2 for x in range(5)} -
dict.get(key, default): Safer access; returnsdefaultif key missing (noKeyError). -
collections.defaultdict: Subclass of dict; calls a factory function to provide default values for missing keys. -
collections.OrderedDict(Python 3.7+): Standarddictpreserves insertion order by default.OrderedDicthas additional ordering methods.
Sets (Unordered, Unique Elements)
-
Creation:
s = {1, 2, 3}ors = set([1, 2, 2])→{1, 2}. Empty set isset(), not{}(which is an empty dict). -
Mathematical Operations:
| Operation | Symbol | Method | | :--- | :--- | :--- | | Union |
\||s1.union(s2)| | Intersection |&|s1.intersection(s2)| | Difference |-|s1.difference(s2)| | Symmetric Difference |^|s1.symmetric_difference(s2)| -
frozenset: Immutable version of a set. Hashable → can be a dictionary key or element of another set.
[!TIP] Exam Focus: Know time complexities for list/dict operations. Distinguish mutable (
list,dict,set) vs immutable (tuple,frozenset) types. Remembersetrequires hashable elements. Shallow copy pitfalls with nested lists are a classic question.
III. Working with Files and I/O
File Opening Modes
| Mode | Meaning | File Pointer Position |
|---|---|---|
'r' |
Read (default) | Start |
'w' |
Write (truncates) | Start (creates new) |
'a' |
Append | End |
'x' |
Exclusive creation | Start (fails if exists) |
'b' |
Binary mode | - |
't' |
Text mode (default) | - |
'+' |
Open for updating (read/write) | - |
- Common Combinations:
'r+'(read/write),'w+'(write/read, truncates),'a+'(append/read).
The with Statement (Context Manager)
-
Purpose: Ensures proper acquisition and release of resources (like files) automatically, even if exceptions occur.
-
Syntax:
with open('file.txt', 'r') as f: data = f.read() # File is automatically closed here
Why it's best practice: Eliminates the need for explicit
try-finallyblocks to close files.
Reading from Files
-
f.read(size=-1): Readsizebytes/characters.-1or omitted reads entire file. -
f.readline(): Read a single line (including trailing newline). -
f.readlines(): Read all lines into a list. -
Iterating:
for line in f:is memory-efficient (reads line by line).
Writing to Files
-
f.write(str): Write a string to the file. -
f.writelines(list_of_strings): Write a sequence of strings. Does not add newlines automatically. -
Newline Handling: In text mode, Python translates
\nto the OS-specific newline on write, and vice-versa on read.
File and Directory Operations (os & pathlib)
-
osModule (Procedural):import os os.getcwd() # Current working directory os.listdir('.') # List files in directory os.mkdir('new_dir') os.remove('file.txt') os.rename('old.txt', 'new.txt') -
pathlib.Path(Object-Oriented - Recommended):from pathlib import Path p = Path('folder/file.txt') p.exists() p.is_file() p.is_dir() p.mkdir(parents=True, exist_ok=True) p.unlink() # Delete file p.rename('new_file.txt')
[!TIP] Exam Focus: Know the
withstatement syntax and its advantage. Distinguish betweenread(),readline(), andreadlines(). Understand thatwritelines()needs explicit newlines. Be able to convert basicoscalls topathlibequivalents.
IV. Error Handling and Exceptions
Exceptions vs. Syntax Errors
-
Syntax Errors: Detected at compile-time (parsing). Code won't run.
-
Exceptions: Detected at runtime. Can be caught and handled.
Common Built-in Exceptions (Overview)
| Exception | Typical Cause |
|---|---|
ValueError |
Right type, inappropriate value (e.g., int('abc')) |
TypeError |
Operation/function applied to wrong type (e.g., 'a' + 1) |
IndexError |
Sequence subscript out of range |
KeyError |
Dictionary key not found |
FileNotFoundError |
open() on non-existent file (mode 'r') |
ZeroDivisionError |
Division/modulo by zero |
AttributeError |
Attribute reference/assignment fails |
ImportError / ModuleNotFoundError |
import fails |
try, except, else, finally Blocks
try:
# Risky code
result = 10 / 0
except ZeroDivisionError as e:
# Handle specific exception
print(f"Error: {e}")
except (TypeError, ValueError):
# Handle multiple exceptions
print("Type or Value error")
except Exception as e:
# Catch-all (use sparingly)
print(f"Unexpected: {e}")
else:
# Executes if NO exception raised in try block
print("Operation successful")
finally:
# ALWAYS executes (cleanup: close files, release locks)
print("Cleanup complete")
- Order: Specific exceptions before general ones.
except:without type catches all exceptions (including system-exiting ones likeKeyboardInterrupt).
The raise Statement
-
Raising Built-in:
raise ValueError("Invalid input") -
Custom Exceptions: Define by inheriting from
Exception(or a subclass).class MyCustomError(Exception): def __init__(self, message): super().__init__(message) raise MyCustomError("Something went wrong")
Exception Chaining and Context
-
raise ... from ...: Explicitly sets the__cause__attribute, showing the original exception that led to the current one.try: int('abc') except ValueError as e: raise RuntimeError("Failed conversion") from e -
__context__: Automatically set when an exception occurs in anexceptblock.
[!TIP] Exam Focus: Structure of
try-except-else-finally. Know whenelsevsfinallyruns. Custom exception definition is a common coding question. Understandraise fromfor preserving traceback.
V. Modules and Packages
The Module System
-
Module: A single
.pyfile containing Python definitions and statements. -
importVariations:import module # Access via module.name from module import name # Access via name directly from module import * # Imports all public names (discouraged) -
sys.path: List of directories Python searches for modules. Current directory is first. -
__name__Variable:-
When a module is run directly:
__name__ == "__main__" -
When a module is imported:
__name__ == "module_name" -
** idiom:**
if __name__ == "__main__":to put test/example code that runs only on direct execution.
-
Creating and Using Packages
-
Package: A directory containing a special
__init__.pyfile (can be empty) and module files/sub-packages.mypackage/ __init__.py module1.py subpackage/ __init__.py module2.py -
Absolute Import:
from mypackage import module1(from project root). -
Relative Import: (Inside a package)
from . import module1(current package),from .. import module2(parent package). Only works within packages.
Standard Library Highlights
-
math:sqrt(),pi,ceil(),floor(),log(). -
random:randint(a, b),choice(seq),shuffle(seq),random(). -
datetime:datetime.now(),date,time,timedelta. -
json:json.dumps(obj)(to string),json.loads(str)(from string),json.dump(obj, file),json.load(file). -
collections:namedtuple,defaultdict,Counter,deque,OrderedDict.
[!TIP] Exam Focus: Purpose of
__name__ == "__main__". Difference between absolute and relative imports. Know basic usage of at least 3 standard library modules (e.g.,jsonfor serialization,datetimefor dates,collections.Counterfor counting).
VI. Intermediate Concepts & Best Practices
Iterators and the iter()/next() Protocol
-
Iterable: An object that can return an iterator. Must implement
__iter__()(returns an iterator object). Examples:list,tuple,dict,set,str. -
Iterator: An object that maintains state and produces the next value on demand. Must implement:
-
__iter__(): Returns the iterator object itself. -
__next__(): Returns the next item. RaisesStopIterationwhen exhausted.
-
-
Relationship:
iter(iterable)callsiterable.__iter__()to get an iterator.next(iterator)callsiterator.__next__().it = iter([1, 2, 3]) print(next(it)) # 1 print(next(it)) # 2
Generators and yield
-
Generator Function: Defined with
defbut containsyieldexpression(s). Returns a generator iterator (a type of iterator). -
yield: Pauses function execution, saves local state, and returns a value. On nextnext(), resumes from where it left off. -
Generator Expression: Similar to list comprehension but with
()instead of[]. Returns a generator object (lazy evaluation).gen_exp = (x**2 for x in range(1000000)) # No list created in memory -
Advantage: Memory efficient for large/streaming data. Values are generated on-the-fly.
Decorators (Conceptual Introduction)
-
A decorator is a function that takes another function as an argument and returns a new function (usually a wrapper) to extend/modify its behavior without permanently modifying it.
-
@decoratorsyntax: Syntactic sugar forfunc = decorator(func).def my_decorator(func): def wrapper(): print("Before function") func() print("After function") return wrapper @my_decorator def say_hello(): print("Hello!") # say_hello() now prints: # Before function # Hello! # After function
Code Quality and Style (PEP 8)
| Convention | Example |
|---|---|
| Naming | snake_case for functions/variables, PascalCase for classes, UPPER_CASE for constants. |
| Indentation | 4 spaces per level (no tabs). |
| Line Length | Max 79 characters (code), 72 (docstrings/comments). |
| Imports | Standard library → third-party → local, each group separated by a blank line. |
| Whitespace | Around operators (except high-priority like **), after commas, but not inside parentheses/brackets. |
| Docstrings | Triple double-quotes ("""). One-liner on same line, multi-line with summary line, blank line, then description. |
[!TIP] Exam Focus: Be able to write a simple generator function using
yield. Understand the basic decorator pattern (wrapperfunction). Know PEP 8 naming conventions and the purpose ofif __name__ == "__main__". Distinguish between an iterable (has__iter__) and an iterator (has__next__).