Functions and Objects
Defining a function
def area(width, height):
return width * height
area(3, 4) # positional
area(width=3, height=4) # keyword — clearer when there are several
return hands a value back and stops the function. No return means None.
Parameters
def greet(name, greeting="Hello"): # default value
def total(*numbers): # extra positionals → tuple
def config(**options): # extra keywords → dict
def mixed(a, b=1, *args, **kwargs): # the full ordering
Parameters with defaults must follow those without.
Never use a mutable default
def broken(item, basket=[]): # the SAME list on every call
...
def fixed(item, basket=None): # do this instead
if basket is None:
basket = []
Spreading at the call site
args = [1, 2, 3]
total(*args) # same as total(1, 2, 3)
opts = {"name": "Ada"}
greet(**opts) # same as greet(name="Ada")
Guard clauses
def safe_average(numbers):
if not numbers:
return None
return sum(numbers) / len(numbers)
Handle the awkward case first and return; the main body stays unindented.
Scope
| Situation | Behaviour |
|---|---|
| name created in a function | local, gone when it returns |
| reading an outer name | allowed |
| assigning to an outer name | creates a new local |
global x | rebinds the module-level name — almost always avoidable |
list.append() on an outer list | works: mutating is not rebinding |
count = 0
def bump():
count = count + 1 # UnboundLocalError
Pass the value in and return the new one instead.
Lambdas and sorting
sorted(words, key=len) # no lambda needed
sorted(people, key=lambda p: p["age"])
sorted(people, key=lambda p: (-p["score"], p["name"])) # two fields
sorted(values, reverse=True)
A tuple key sorts by the first element, then the second as a tie-breaker. Negating a number reverses just that field.
list(map(str.upper, words))
list(filter(None, values)) # drop falsy values
A comprehension usually reads better than map or filter.
Classes
class Dog:
def __init__(self, name, breed):
self.name = name
self.breed = breed
def speak(self):
return f"{self.name} says woof"
rex = Dog("Rex", "collie")
rex.speak()
__init__runs when an instance is created.selfis the instance; Python passes it for you.- Attributes go on
self, inside__init__.
Never put a mutable value in the class body — every instance would share it:
class Bad:
items = [] # shared by all instances
class Good:
def __init__(self):
self.items = [] # one per instance
Inheritance
class Animal:
def __init__(self, name):
self.name = name
class Cat(Animal):
def __init__(self, name, indoor):
super().__init__(name) # run the parent's setup
self.indoor = indoor
def speak(self): # override
return "Meow"
isinstance(obj, Animal) is True for a Cat. Reach for inheritance when the child
genuinely is a kind of the parent.
Dunder methods
| Method | Called by |
|---|---|
__init__ | creating an instance |
__str__ | print(obj), str(obj) |
__repr__ | the console, debugging |
__eq__ | a == b |
__len__ | len(obj) |
__lt__ | a < b, and sorting |
__contains__ | x in obj |
Without __eq__, two objects with identical contents are not equal — Python falls
back to comparing identity.
Dataclasses
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float
quantity: int = 1
tags: list = field(default_factory=list) # the safe mutable default
def total(self):
return self.price * self.quantity
You get __init__, __repr__ and __eq__ written for you. Fields with defaults
come last, and field(default_factory=list) is how a dataclass avoids the shared-list
trap.