Basics for a new bee. From print() to classes, with the gotchas nobody tells you about.
Run your first program
Variables, types, operators
Slice, format, methods
if, for, while
list, dict, tuple, set
def, args, return
try, except, raise
open, import, stdlib
Classes and the 4 pillars
The traps, up front
Python was created by Guido van Rossum and first released in 1991. Free, open source, and readable on purpose.
Python is an interpreter. You hand it source text, it executes it line by line. There is no separate compile step to worry about.
$ python3 --version Python 3.12.4
On Mac and Linux the command is python3. On Windows it is usually python or py.
$ python3 >>> 2 + 2 4 >>> name = "bee" >>> name.upper() 'BEE' >>> exit()
REPL = Read, Eval, Print, Loop. This is where you should test every idea in this deck.
# hello.py print("Hello, World")
$ python3 hello.py Hello, World
$ python3 -m venv .venv # create $ source .venv/bin/activate # mac/linux $ .venv\Scripts\activate # windows (.venv) $ pip install requests (.venv) $ pip freeze > requirements.txt (.venv) $ deactivate
A venv is a private folder of packages for one project. Without it, every project shares one global set of libraries and they eventually collide. Make one before you install anything.
if True: print("inside the block") # 4 spaces print("outside the block")
Other languages use { }. Python uses whitespace. The rule: 4 spaces per level, never tabs. Mixing tabs and spaces raises TabError.
>>> help(len) >>> dir("text") # every method on str >>> "text".upper.__doc__
print() writes objects to standard output as text. input() reads a line back from the user, always as a string.
print("Hello World") # many arguments, joined by a space print("a", 1, True, [1, 2])
end= — what to print after# default is a newline; override it print("Welcome to", end=' ') print("Python")
sep= — what goes between argumentsprint('09', '12', '2016', sep='-') print('user', 'example.com', sep='@')
val = input("Enter your value: ") print(val, type(val))
age = input("Age: ") # "30" the string print(age + 1)
Fix: convert it yourself.
age = int(input("Age: ")) print(age + 1) # 31
import sys print("went wrong", file=sys.stderr) with open("log.txt", "w") as f: print("line one", file=f)
Comments are ignored by the interpreter. They exist for the next person to read the code, which is usually you in three months.
## This whole line is a comment total = 42 # and this trailing part is too
# Python has no /* */ block comment. # You just stack # lines. # Most editors do this with Cmd+/.
""" This is technically a string expression, not a comment. Python evaluates it and throws it away. """ name = "bee" print(name)
People use these as block comments. It works, but the real job of triple quotes is the docstring below.
def area(width, height): """Return the area of a rectangle. Args: width: length of one side. height: length of the other. Returns: width * height """ return width * height
A string as the first statement of a module, function, or class becomes its docstring. Tools read it.
>>> help(area) >>> area.__doc__
# BAD: narrates the obvious i = i + 1 # add one to i # GOOD: explains the reasoning # Retry once; the upstream API drops # the first request after an idle gap. attempts = 2
Conventions you will see in real code: # TODO(name): for tracked follow-ups, # HACK: for a deliberate shortcut.
Delete it. Version control remembers it for you. Dead commented blocks rot and mislead readers.
You never declare a type. You bind a name to an object with =, and the object carries the type. Rebind the name whenever you like.
x = "Hello World" # str x = 50 # now int x = 60.5 # now float x = 3j # now complex # Multiple at once a, b, c = 1, 2, 3 x = y = z = 0 # Swap, no temp needed a, b = b, a
| Type | Literal | Note |
|---|---|---|
int | 50, -7, 1_000_000 | Unlimited size |
float | 60.5, 2e-3 | 64-bit, inexact |
complex | 3j | Real + imaginary |
bool | True, False | Subclass of int |
str | "bee" | Immutable text |
NoneType | None | "No value" |
list | [1, 2] | Mutable sequence |
tuple | (1, 2) | Immutable sequence |
dict | {"k": 1} | Key to value |
set | {1, 2} | Unique, unordered |
This picture explains most surprises later in the deck: b = a copies the arrow, not the object.
| Thing | Style | Example |
|---|---|---|
| Variable / function | snake_case | max_retries |
| Constant | UPPER_SNAKE | DEFAULT_TIMEOUT |
| Class | PascalCase | DataProcessor |
| Internal | _leading | _cache |
Legal: letters, digits, underscore. Cannot start with a digit. Case sensitive. Do not shadow built-ins such as list, str, id, type, sum.
Python is dynamically typed (a name can hold anything) but strongly typed (it will not silently add a string to a number for you).
x = 50 print(type(x)) # <class 'int'> # Prefer isinstance for checks; # it respects inheritance. isinstance(x, int) # True isinstance(x, (int, float)) # True
int("42") # 42 int(3.99) # 3 truncates, no rounding float("3.14") # 3.14 str(42) # '42' bool(0) # False list("abc") # ['a', 'b', 'c'] int("ff", 16) # 255 base 16 round(3.567, 2) # 3.57
int("twelve")
Wrap user input in try / except ValueError. See the errors slide.
Every object can be used in an if. These are the only falsy built-ins:
bool(None) # False bool(False) # False bool(0) # False also 0.0, 0j bool("") # False empty string bool([]) # False empty list bool({}) # False empty dict/set bool(()) # False empty tuple # Everything else is True bool("False") # True (non-empty string!) bool([0]) # True (non-empty list) bool(-1) # True
So write if items: rather than if len(items) > 0:. It is the idiom.
def greet(name: str, times: int = 1) -> str: return ("hi " + name + " ") * times
Hints do nothing at runtime. Python does not enforce them. They are documentation that editors and tools such as mypy can check. You will see them everywhere in production code.
Operators act on values and variables. Six families cover almost everything you will write.
+ | add | 7 + 2 = 9 |
- | subtract | 7 - 2 = 5 |
* | multiply | 7 * 2 = 14 |
/ | true divide | 7 / 2 = 3.5 |
// | floor divide | 7 // 2 = 3 |
% | remainder | 7 % 2 = 1 |
** | power | 7 ** 2 = 49 |
/ always gives a float, even 4 / 2 = 2.0.
n = 10 n += 5 # 15 n -= 3 # 12 n *= 2 # 24 n //= 5 # 4 n **= 2 # 16 # walrus: assign inside an expression if (size := len(data)) > 10: print(size)
== | equal values |
!= | not equal |
> < | greater / less |
>= <= | or equal |
5 == 5.0 # True "a" < "b" # True (alphabetical) [1,2] == [1,2] # True # Chaining works and reads well 0 < age < 130
a and b # both true? a or b # either true? not a # flip # Short-circuit: stops early. # If items is empty, items[0] # is never evaluated. if items and items[0] == "x": ...
"ee" in "bee" # True 3 in [1, 2, 3] # True "k" in {"k": 1} # True (keys) 5 not in [1, 2] # True x is None # same object? x is not None
== asks "same value". is asks "same object in memory". Use is only for None, True, False.
& | AND | 6 & 3 = 2 |
| | OR | 6 | 3 = 7 |
^ | XOR | 6 ^ 3 = 5 |
~ | NOT | ~6 = -7 |
<< | shift left | 6 << 1 = 12 |
>> | shift right | 6 >> 1 = 3 |
** then unary - then * / // % then + - then comparisons then not then and then or. When in doubt, add parentheses. Nobody has ever complained about clear grouping.
A string is an ordered sequence of characters. Once created it cannot be changed. Every "modification" returns a new string.
a = 'single quotes' b = "double quotes" # identical c = """spans multiple lines""" d = "say \"hi\" and a\ttab" # escapes e = r"C:\new\path" # raw, no escapes f = "ab" * 3 # 'ababab' g = "a" + "b" # 'ab'
s = "BeePostive" s[0] # 'B' first s[3] # 'P' s[-1] # 'e' last s[-2] # 'v' second last len(s) # 10 s[99] # IndexError
s = "bee" s[0] = "B" # TypeError! # Build a new one instead s = "B" + s[1:] # 'Bee' s = s.replace("b", "B") # 'Bee'
s[start:stop:step]s = "BeePostive" s[3:-2] # 'Posti' 3 to 2-from-end s[:3] # 'Bee' from the start s[3:] # 'Postive' to the end s[:] # whole copy s[::2] # 'BePsie' every 2nd s[::-1] # 'evitsoPeeB' reversed s[100:] # '' slices never IndexError
Indexing out of range raises. Slicing out of range just gives you what exists. Same rules apply to lists and tuples.
Methods never modify the original. They return a new string, so you must assign the result.
| Call | Result |
|---|---|
"Bee".upper() | 'BEE' |
"Bee".lower() | 'bee' |
" x ".strip() | 'x' |
"a,b,c".split(",") | ['a','b','c'] |
"-".join(["a","b"]) | 'a-b' |
"bee".replace("e","3") | 'b33' |
"bee".find("e") | 1 (-1 if absent) |
"bee".count("e") | 2 |
"bee".startswith("b") | True |
"bee".endswith("e") | True |
"42".isdigit() | True |
"bee".title() | 'Bee' |
"7".zfill(3) | '007' |
s = " bee " s.strip() # computed, thrown away print(s) # still ' bee ' s = s.strip() # correct
name, score = "bee", 93.4567 print(f"Hi {name}, you scored {score}") print(f"Rounded: {score:.2f}") print(f"Padded: {name:>10}|") print(f"Percent: {0.876:.1%}") print(f"Commas: {1234567:,}") print(f"Expression: {score * 2:.0f}") print(f"Debug: {score=}")
# .format() — pre-3.6 code and templates "Result is {}".format(5) "{name} is {age}".format(name="bee", age=3) # % — very old, avoid in new code "Result is %d" % 5
Use f-strings. They are faster and you read the value right where it appears.
Run different blocks depending on a condition. The colon opens the block, the indentation defines it.
x = 10 if x > 5: print("x is greater than 5") elif x == 5: print("x is 5") else: print("x is less than 5")
Only the first matching branch runs. elif can repeat any number of times. else is optional.
# Hard to follow if user: if user.active: if user.admin: grant() # Guard clauses — flat and readable if not user: return if not user.active: return if not user.admin: return grant()
items = [] if items: # idiomatic print("has items") else: print("empty") # Not this if len(items) > 0: ... if items != []: ...
status = "adult" if age >= 18 else "minor" # Reads as: value_if_true if cond else value_if_false print(f"{n} item{'s' if n != 1 else ''}")
Fine for a short choice. Do not chain three of them together.
match — Python 3.10 and latermatch command: case "start": run() case "stop" | "halt": # either halt() case _: # default print("unknown")
A cleaner switch for many fixed cases. Optional knowledge on day one, but you will see it.
Use for when you know what you are iterating over. Use while when you are waiting for a condition to change.
for i in range(5): print(i) # prints 0 to 4 for ch in "bee": print(ch) # b, e, e for item in ["a", "b", "c"]: print(item) for key, value in {"a": 1}.items(): print(key, value)
There is no C-style for (i=0; i<n; i++). Python loops over the items themselves.
range(5) # 0 1 2 3 4 range(2, 6) # 2 3 4 5 range(0, 10, 2) # 0 2 4 6 8 range(5, 0, -1) # 5 4 3 2 1 print(list(range(5)))
stop is excluded, same rule as slicing. range is lazy: it generates numbers on demand, so range(10**9) uses no memory.
i = 0 while i < 5: print(i) i += 1 # do not forget this
Forget the increment and you have an infinite loop. Ctrl+C stops it.
while True: line = input("> ") if line == "quit": break print("you said", line)
Deliberate infinite loop plus break. This is normal and idiomatic when the exit condition is in the middle.
for row in range(3): for col in range(3): print(row, col, end=" ") print()
A break exits only the innermost loop.
for i in range(10): if i == 5: break # leave the loop entirely if i == 3: continue # jump to next iteration print(i)
3 is skipped by continue. 5 onward never runs because break fired.
else on a loop — the surprising onefor n in [1, 3, 5]: if n % 2 == 0: print("found an even") break else: print("no even numbers at all")
The else runs only if the loop finished without hitting break. Read it as "no break".
vowels = ['a', 'e', 'i', 'o', 'u'] for i, letter in enumerate(vowels): print(i, letter)
# Start counting from 1 for i, letter in enumerate(vowels, start=1): print(i, letter)
Never write for i in range(len(x)) just to get an index. This is the replacement.
names = ["bee", "ant", "fly"] legs = [6, 6, 6] for name, n in zip(names, legs): print(name, n) # zip stops at the shorter one list(zip([1,2,3], ["a","b"])) # [(1, 'a'), (2, 'b')]
reversed([1,2,3]) # 3, 2, 1 sorted([3,1,2]) # [1, 2, 3] sorted(words, key=len) # by length sorted(x, reverse=True) # descending
The workhorse collection. An ordered sequence you can grow, shrink, and rearrange in place.
var = ["Bee", "Post", "ive"] print(var)
mixed = [1, "two", 3.0, [4], None] # any types empty = [] built = list("abc") # ['a','b','c'] var[0] # 'Bee' var[-1] # 'ive' var[0:2] # ['Bee', 'Post'] slicing works len(var) # 3 "Bee" in var # True
x = [3, 1, 2] x[0] = 99 # [99, 1, 2] x.append(4) # add one at the end x.extend([5, 6]) # add many x.insert(0, 0) # at a position x.remove(99) # by value, first match last = x.pop() # remove and return last first = x.pop(0) # by index del x[0] # by index, no return x.sort() # in place, returns None x.reverse() # in place x.clear() # empty it
x = [3, 1, 2] y = x.sort() # y is None! # x is now [1,2,3], but y is nothing y = sorted(x) # y is a NEW sorted list # x is unchanged
The rule across Python: methods that mutate in place return None. Same for append, reverse, extend.
a = [1, 2, 3] b = a # NOT a copy, same object b.append(4) print(a) # [1, 2, 3, 4] surprise # Real copies b = a.copy() # or a[:] or list(a) # For nested lists, go deep import copy b = copy.deepcopy(a)
x = [5, 3, 5, 1] sum(x) # 14 min(x), max(x) # 1, 5 x.count(5) # 2 x.index(3) # 1 sorted(set(x)) # [1, 3, 5] dedupe + sort ", ".join(["a","b"]) # 'a, b' (strings only)
A tuple is a list that cannot change after creation. Use it for a fixed group of values that belong together.
var = ("Bee", "Post", "ive") print(var)
point = (3, 4) point[0] # 3 index like a list point[0:1] # (3,) slice like a list len(point) # 2 3 in point # True point[0] = 9 # TypeError: does not support # item assignment
x = (5) # just the int 5 in brackets! type(x) # <class 'int'> x = (5,) # the trailing comma makes it type(x) # <class 'tuple'> # The comma is what builds a tuple, # not the parentheses. y = 1, 2, 3 # (1, 2, 3)
# pack person = ("bee", 3, "hive") # unpack — counts must match name, age, home = person # swap without a temp variable a, b = b, a # star grabs the rest first, *rest = [1, 2, 3, 4] # first = 1, rest = [2, 3, 4] # ignore what you do not need name, _, _ = person
def min_max(values): return min(values), max(values) lo, hi = min_max([4, 9, 1]) print(lo, hi)
"Multiple return values" in Python is really one tuple, packed on the way out and unpacked on the way in.
grid = {(0, 0): "origin", (1, 2): "target"}
A mapping from keys to values. Lookup by key is fast no matter how large the dictionary gets. This is the most useful data structure in Python.
d = {1: 'Bee', 2: 'For', 3: 'Bee'}
print(d)
user = {"name": "bee", "age": 3}
empty = {}
built = dict(name="bee", age=3)
pairs = dict([("a", 1), ("b", 2)])
user["name"] # 'bee'
user["missing"] # KeyError!
user.get("missing") # None, no error
user.get("missing", 0) # 0, your default
"name" in user # True (checks keys)
len(user) # 2
user["email"] = "b@hive.io" # add or overwrite user.update({"age": 4}) # merge a dict in user.setdefault("tags", []) # set only if absent del user["email"] # KeyError if absent user.pop("email", None) # safe remove user.clear() # empty it # Merge two dicts (3.9+) merged = defaults | overrides
d = {"a": 1, "b": 2}
for k in d: # keys by default
print(k)
for v in d.values():
print(v)
for k, v in d.items(): # the common one
print(k, "=", v)
list(d.keys()) # ['a', 'b'] sorted(d) # ['a', 'b'] sorts keys
ok = {"str": 1, 42: 2, (1, 2): 3} # fine
bad = {[1, 2]: "x"} # TypeError:
# unhashable type: 'list'
Keys must be immutable: strings, numbers, tuples of immutables. Values can be anything, including lists and other dicts.
OrderedDict.{"a": 1, "a": 2} is {"a": 2}. The last write wins.A bag of distinct items with no order. Built for two questions: "have I seen this?" and "what do these two groups share?"
var = {"Bee", "For", "Bee"}
print(var)
The duplicate vanished, and the print order is not the order you typed. Both are expected.
s = set([1, 2, 2, 3]) # {1, 2, 3} s = set("hello") # {'h','e','l','o'}
x = {} # this is an empty DICT
type(x) # <class 'dict'>
x = set() # this is an empty SET
s = {1, 2}
s.add(3) # {1, 2, 3}
s.add(3) # no change, no error
s.discard(9) # safe, no error if absent
s.remove(9) # KeyError if absent
s.update([4, 5]) # add many
2 in s # True, and very fast
a = {1, 2, 3}
b = {3, 4, 5}
a | b # {1,2,3,4,5} union, either
a & b # {3} intersection, both
a - b # {1,2} in a, not in b
a ^ b # {1,2,4,5} in exactly one
a <= b # is a a subset of b? False
a.isdisjoint(b) # no shared items? False
# Dedupe a list, keep it simple unique = list(set(items)) # Dedupe and sort unique = sorted(set(items)) # Membership test in a big collection seen = set() for item in stream: if item in seen: # fast, even at 1M items continue seen.add(item) process(item)
x in a_list scans every element. x in a_set is a single hash lookup. On large data that difference is the whole ballgame.
Most beginner code reaches for a list every time. Picking correctly makes code both faster and clearer about intent.
| Type | Literal | Ordered | Mutable | Duplicates | Lookup by value | Reach for it when |
|---|---|---|---|---|---|---|
list | [1, 2] |
Yes | Yes | Yes | Slow (scan) | A sequence you will append to, sort, or index |
tuple | (1, 2) |
Yes | No | Yes | Slow (scan) | A fixed record, or a dict key |
dict | {"k": 1} |
Yes (insertion) | Yes | Keys unique | Fast by key | You look things up by a name or id |
set | {1, 2} |
No | Yes | No | Fast | Uniqueness, or "have I seen this?" |
dictsettuplelistlist({1, 2}) # set → list set([1, 1, 2]) # list → set tuple([1, 2]) # list → tuple list(d.items()) # dict → pairs dict(pairs) # pairs → dict
from collections import ( Counter, defaultdict, deque) Counter("hello") # {'l':2,'h':1,'e':1,'o':1} d = defaultdict(list) d["new"].append(1) # no KeyError q = deque() # fast pops at both ends
A compact way to say "make a new collection by running an expression over each item of an old one". Once you can read them, half of real Python opens up.
# The long way squares = [] for n in range(5): squares.append(n * n) # The same thing squares = [n * n for n in range(5)] print(squares)
Shape: [ expression for item in iterable ]
# keep only what passes the if evens = [n for n in range(10) if n % 2 == 0] # [0, 2, 4, 6, 8] names = [u["name"] for u in users if u["active"]] # if/else goes BEFORE the for labels = ["even" if n % 2 == 0 else "odd" for n in range(4)]
matrix = [[1, 2], [3, 4]] # flatten: read the fors left to right flat = [x for row in matrix for x in row] # [1, 2, 3, 4]
# LIST — square brackets [c for c in [1, 2, 3]] # [1, 2, 3] # SET — curly braces, dedupes {c for c in "hello"} # {'h', 'e', 'l', 'o'} # DICT — key: value pairs keys = ['a','b','c','d','e'] values = [1,2,3,4,5] myDict = {k: v for (k, v) in zip(keys, values)} print(myDict)
# GENERATOR — parentheses, lazy gen = (n * n for n in range(1_000_000)) sum(gen) # computes one at a time, # never builds the list
# Unreadable. Just write the loop. r = [f(x, y) for x in a if p(x) for y in b if q(x, y) and r(y)]
A comprehension should fit on one or two lines and do one transformation. Past that, a plain for loop is the better code.
A function groups statements that serve one purpose, so you can run them on different inputs instead of copying the code.
# A simple Python function def fun(): print("Welcome to BeePostive") # Nothing happens until you call it fun()
def name ( params ) colon, then an indented body. Define before you call.
# Check whether x is even or odd def even_odd(x): if x % 2 == 0: print("even") else: print("odd") even_odd(2) even_odd(3)
def add(a, b): return a + b # hands the value back def is_true(a): return bool(a) print(f"add is {add(2, 3)}") print(f"is_true is {is_true(2 < 5)}")
def shout(msg): print(msg.upper()) # prints, returns nothing result = shout("hi") print(result) # None
No return means return None. print shows a value, return hands it back. Confusing the two is the single most common beginner mistake.
def func(): return 1, 2, 3, 4, 5 one, two, three, four, five = func() print(one, two, three, four, five)
The values are packed into a tuple on the way out and unpacked on the way in.
def double(n): return n * 2 f = double # no parens: the function itself f(5) # 10 list(map(double, [1, 2])) # [2, 4] sorted(words, key=len) # pass a function in # lambda: a tiny unnamed function sorted(users, key=lambda u: u["age"])
Arguments are the values you pass between the parentheses. Python gives you several ways to accept them, from strict to wide open.
def connect(host, port, timeout=30): ... connect("db.local", 5432) # positional connect("db.local", port=5432) # keyword connect(port=5432, host="db.local") # any order connect("db.local", 5432, 5) # override default # Defaults must come last def bad(a=1, b): ... # SyntaxError
*def connect(host, port, *, use_tls=True): ... connect("db", 443, use_tls=False) # ok connect("db", 443, False) # TypeError
Everything after the * must be passed by name. Use this for flags, so call sites never read as f(x, True, False, True).
def my_fun(arg1, arg2, arg3): print("arg1:", arg1) print("arg2:", arg2) print("arg3:", arg3) args = ("Bee", "for", "Bee") my_fun(*args) # spread a sequence kwargs = {"arg1": "Bee", "arg2": "for", "arg3": "Bee"} my_fun(**kwargs) # spread a dict
*args — any number of positionalsdef total(*args): print(type(args)) # <class 'tuple'> return sum(args) total(1, 2, 3) # 6 total() # 0
Inside the function args is a plain tuple of whatever was passed.
**kwargs — any number of nameddef show(**kwargs): print(type(kwargs)) # <class 'dict'> for k, v in kwargs.items(): print(k, "=", v) show(host="db", port=5432)
The names args and kwargs are convention. The * and ** are what matter.
def f(pos, /, normal, *args, kwonly, **kwargs): ...
pos | before /: positional only |
normal | positional or keyword |
*args | extra positionals → tuple |
kwonly | after *: keyword only |
**kwargs | extra keywords → dict |
You rarely need all five. Recognising them when you read library code is enough.
Two ideas explain most confusing Python behaviour: where a name is visible, and whether the object behind it can change.
count = 0 # module level (global) def bump(): count = 99 # a NEW local name bump() print(count) # still 0
Assigning inside a function creates a local name. Python looks up names Local → Enclosing → Global → Built-in.
def bump(): global count # works, but avoid count += 1 # Better: take it in, hand it back def bump(count): return count + 1
LIMIT = 10 def check(n): return n < LIMIT # reads global, fine
# BROKEN def add_item(item, items=[]): items.append(item) return items print(add_item("a")) # ['a'] print(add_item("b")) # ['a', 'b'] !!
The default list is created once, when the function is defined, and then shared by every call that omits it.
# FIXED def add_item(item, items=None): if items is None: items = [] items.append(item) return items
def wipe(values): values.clear() # mutates the caller's list def rebind(values): values = [] # only rebinds the local name data = [1, 2, 3] rebind(data) print(data) # [1, 2, 3] untouched wipe(data) print(data) # [] gone
Mutable arguments (list, dict, set, objects) can be changed by the function. Immutable ones (int, str, tuple) cannot.
Python ships around 70 built-ins. These are the ones you will use in your first month. If you find yourself writing a loop, check this list first.
len(x) | item count |
sum(x) | total |
min(x) max(x) | extremes |
abs(-3) | 3 |
round(3.567, 2) | 3.57 |
pow(2, 8) | 256 |
divmod(7, 2) | (3, 1) |
int float str bool | scalars |
list tuple dict set | collections |
range(5) | lazy numbers |
enumerate(x) | index + value |
zip(a, b) | pair up |
sorted(x) | new sorted list |
reversed(x) | backwards |
map(f, x) | apply f to each |
filter(f, x) | keep where f true |
any(x) | any truthy? |
all(x) | all truthy? |
any([False, True]) # True all([True, True]) # True all([]) # True (vacuously)
print(x) | write out |
input(p) | read a line |
open(path) | a file object |
type(x) | its class |
isinstance(x, T) | type check |
dir(x) | its attributes |
help(x) | its docs |
id(x) | identity |
repr(x) | debug string |
# Do not hand-roll this total = 0 for n in nums: total += n # It already exists, in C total = sum(nums)
Built-ins are implemented in C, so they are faster than the equivalent Python loop as well as shorter.
When something goes wrong Python raises an exception. Unhandled, it stops the program and prints a traceback. Read tracebacks from the bottom up.
Last line = what went wrong. Line above it = where. Everything above that is how you got there.
SyntaxError | Code will not even parse |
IndentationError | Bad whitespace |
NameError | Undefined name, often a typo |
TypeError | Wrong type for the operation |
ValueError | Right type, bad value |
IndexError | List index out of range |
KeyError | Dict key missing |
AttributeError | No such method or field |
FileNotFoundError | Bad path |
ZeroDivisionError | Divided by 0 |
try: age = int(input("Age: ")) except ValueError: print("That was not a number") age = 0 else: print("Parsed fine") # no exception finally: print("Always runs") # cleanup
try | code that might fail |
except | runs only on that error |
else | runs if nothing failed |
finally | runs either way |
# Catch several, capture the object except (ValueError, TypeError) as err: print(f"failed: {err}")
try: risky() except: # catches EVERYTHING, pass # then hides it. Undebuggable.
Catch the specific exception you can actually handle, and at minimum log the rest.
def set_age(n): if n < 0: raise ValueError(f"age cannot be {n}") # Your own exception type class ConfigError(Exception): """Config is missing or invalid.""" try: parse() except ValueError as err: raise ConfigError("bad config") from err
Open a file, use it, close it. The with statement closes it for you, even if an exception is raised in the middle.
with open("data.txt", "r", encoding="utf-8") as f: content = f.read() # file is closed here, guaranteed print(content)
Always pass encoding="utf-8". Without it Python uses a platform default and the same code behaves differently on another machine.
with open("data.txt", encoding="utf-8") as f: everything = f.read() # one big string with open("data.txt", encoding="utf-8") as f: lines = f.readlines() # list of lines # Best for big files: stream line by line with open("data.txt", encoding="utf-8") as f: for line in f: print(line.rstrip("\n"))
Iterating the file object never loads the whole thing into memory.
# "w" truncates the file first! with open("out.txt", "w", encoding="utf-8") as f: f.write("first line\n") f.writelines(["a\n", "b\n"]) # "a" appends to the end with open("out.txt", "a", encoding="utf-8") as f: f.write("appended\n")
| Mode | Means | If missing |
|---|---|---|
"r" | read (default) | Error |
"w" | write, erase first | Created |
"a" | append at end | Created |
"x" | create only | Created |
"rb" "wb" | binary bytes | — |
Opening with "w" deletes the existing contents immediately. Use "a" when you meant to add.
from pathlib import Path p = Path("data") / "input.txt" # cross-platform p.exists() p.suffix # '.txt' p.stem # 'input' text = p.read_text(encoding="utf-8") p.write_text("hello", encoding="utf-8") for f in Path("logs").glob("*.log"): print(f.name)
import json # Python object → file with open("cfg.json", "w", encoding="utf-8") as f: json.dump({"debug": True}, f, indent=2) # file → Python object with open("cfg.json", encoding="utf-8") as f: cfg = json.load(f) json.dumps(obj) # to a string json.loads(text) # from a string
Any .py file is a module. Import it to use its names. Python ships with a large standard library, so check there before installing anything.
import math # math.sqrt(9) import numpy as np # np.array(...) from math import sqrt, pi # sqrt(9) from pathlib import Path # Path("x") from math import * # never do this
Wildcard imports dump unknown names into your namespace and silently shadow your own variables.
# utils.py def greet(name): return f"hi {name}" # main.py, same folder from utils import greet print(greet("bee"))
if __name__ == "__main__"def main(): print("running as a script") if __name__ == "__main__": main()
Run the file directly and __name__ is "__main__", so main() fires. Import the same file from elsewhere and it does not. This is how a file can be both a script and a library.
import json # 1. standard library from pathlib import Path import requests # 2. third party from myapp.utils import greet # 3. your code
| Module | For |
|---|---|
math | sqrt, floor, pi, log |
random | choice, randint, shuffle |
datetime | Dates, times, differences |
json | Read and write JSON |
pathlib | File paths done properly |
os, sys | Environment, argv, exit |
collections | Counter, defaultdict, deque |
itertools | chain, groupby, combinations |
re | Regular expressions |
csv | Comma separated files |
logging | Real logging, not print |
unittest | Built-in test framework |
secrets | Tokens and keys, not random |
import random, datetime from collections import Counter random.choice(["a", "b"]) # 'b' random.randint(1, 6) # 4 today = datetime.date.today() print(today.isoformat()) # '2026-08-21' Counter("mississippi").most_common(2) # [('i', 4), ('s', 4)]
(.venv) $ pip install requests (.venv) $ pip list (.venv) $ pip freeze > requirements.txt (.venv) $ pip install -r requirements.txt
Install into an activated venv, never system-wide. Commit requirements.txt so someone else can rebuild your environment.
Object oriented programming joins data and the functions that act on it into one unit. A class is the blueprint. An object is one thing built from it.
class Dog: species = "Canis familiaris" # class attribute, # shared by all def __init__(self, name, age): self.name = name # instance attributes, self.age = age # one set per object def speak(self): return f"{self.name} says woof" def __repr__(self): return f"Dog({self.name!r}, {self.age})"
a = Dog("Rex", 3) # calls __init__ b = Dog("Ada", 5) print(a.name) # Rex print(a.speak()) # Rex says woof print(a) # Dog('Rex', 3) via __repr__ print(b.species) # shared class attribute a.age = 4 # attributes are mutable isinstance(a, Dog) # True
self?The instance itself, passed in automatically. a.speak() is really Dog.speak(a). You must write self as the first parameter of every instance method, and use self.x to reach that object's data. It is a naming convention, not a keyword, but never rename it.
__init__ | runs at construction |
__repr__ | debug text, for developers |
__str__ | print() text, for users |
__len__ | makes len(obj) work |
__eq__ | defines == |
"Dunder" = double underscore. Python calls these for you.
from dataclasses import dataclass @dataclass class Point: x: int y: int = 0 p = Point(1, 2) print(p) # Point(x=1, y=2)
You get __init__, __repr__, and __eq__ free. No boilerplate.
class Car: def __init__(self, make, model, year): self._make = make # protected, by convention self.__model = model # private, name mangled self.year = year # public def get_make(self): # getter return self._make def set_model(self, model): # setter self.__model = model def get_model(self): return self.__model c = Car("Honda", "Civic", 2024) c.year # 2024 public, free access c.get_make() # 'Honda' via getter c.set_model("Jazz") c.get_model() # 'Jazz' c.__model # AttributeError c._Car__model # 'Jazz' mangling, not security
One underscore says "internal, please leave alone". Two triggers name mangling so subclasses do not clash. Neither is enforced. Python trusts you.
@propertyclass Car: @property def make(self): return self._make c.make # no parentheses, reads like a field
class Animal: def __init__(self, name): self.name = name def speak(self): return "..." class Dog(Animal): # Dog IS AN Animal def __init__(self, name, breed): super().__init__(name) # run parent setup self.breed = breed def speak(self): # override return "woof" d = Dog("Rex", "lab") d.name # 'Rex' inherited d.speak() # 'woof' overridden isinstance(d, Animal) # True
class Cat(Animal): def speak(self): return "meow" for a in [Dog("Rex", "lab"), Cat("Tom")]: print(a.speak()) # woof, then meow
The loop does not care which class it has. Python takes this further with duck typing: if an object has a .speak(), it works, inheritance or not.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): ... class Square(Shape): def __init__(self, s): self.s = s def area(self): return self.s ** 2 Shape() # TypeError: abstract Square(3).area() # 9
Callers depend on "a Shape has an area()", not on how any particular shape computes it.
Every one of these bites a new Python programmer at least once. Read them now and recognise them later.
def f(x, acc=[]): # shared! def f(x, acc=None): # fix
b = a is not a copyb = a # same list object b = a.copy() # actual copy b = copy.deepcopy(a) # nested
for x in items: items.remove(x) # skips items for x in items[:]: # iterate a copy items.remove(x) items = [x for x in items if keep(x)]
is vs ==a = [1]; b = [1] a == b # True same value a is b # False different objects
Use is only with None, True, False.
0.1 + 0.2 == 0.3 # False! 0.1 + 0.2 # 0.30000000000000004 round(a - b, 9) == 0 # ok math.isclose(a, b) # better Decimal("0.1") # money
/ always gives a float4 / 2 # 2.0 not 2 7 // 2 # 3 integer division -7 // 2 # -4 floors, not truncates
x = x.sort() # x is now None x.sort() # correct y = sorted(x) # or this
{} is a dict, not a setempty_dict = {}
empty_set = set()
# O(n^2): new string every time s = "" for w in words: s += w # O(n): the right way s = "".join(words)
list = [1, 2] # now list() is broken list("abc") # TypeError
Also watch str, dict, id, sum, type, input, next.
print(f"{x=} {type(x)=}")breakpoint() drops you into a debuggerint, str, list, dict, tuple, set*args, and **kwargssnake_case for functions and variables, PascalCase for classespip install ruff, then ruff format . and ruff check .| Topic | Why it matters |
|---|---|
Type hints + mypy | Catch whole classes of bugs before running |
pytest | Tests are how you change code without fear |
Generators, yield | Process data larger than memory |
| Decorators | You will meet @property, @cache, framework routes |
logging | Replaces print in anything real |
itertools, functools | The rest of the standard toolkit |
asyncio | Concurrency for I/O bound work |
| Packaging | pyproject.toml, publishing, layouts |
dict or Counter)Questions?
Open a REPL and try the thing you are unsure about. That is the whole method.