01

Python 101

Basics for a new bee. From print() to classes, with the gotchas nobody tells you about.

Setup

Run your first program

Values

Variables, types, operators

Strings

Slice, format, methods

Control Flow

if, for, while

Collections

list, dict, tuple, set

Functions

def, args, return

Errors

try, except, raise

Files & Modules

open, import, stdlib

OOP

Classes and the 4 pillars

Gotchas

The traps, up front

Python was created by Guido van Rossum and first released in 1991. Free, open source, and readable on purpose.

02 / Setup

Running Python THREE WAYS

Python is an interpreter. You hand it source text, it executes it line by line. There is no separate compile step to worry about.

1. Check what you have

$ python3 --version
Python 3.12.4

On Mac and Linux the command is python3. On Windows it is usually python or py.

2. The REPL — try things instantly

$ 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.

3. A script file

# hello.py
print("Hello, World")
$ python3 hello.py
Hello, World

Virtual environments — do this per project

$ 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.

Indentation is the syntax

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.

Getting help without leaving the REPL

>>> help(len)
>>> dir("text")      # every method on str
>>> "text".upper.__doc__
03 / Basics

Output and Input print() AND input()

print() writes objects to standard output as text. input() reads a line back from the user, always as a string.

print basics

print("Hello World")

# many arguments, joined by a space
print("a", 1, True, [1, 2])
Hello World a 1 True [1, 2]

end= — what to print after

# default is a newline; override it
print("Welcome to", end=' ')
print("Python")
Welcome to Python

sep= — what goes between arguments

print('09', '12', '2016', sep='-')
print('user', 'example.com', sep='@')
09-12-2016 user@example.com

input returns a string, always

val = input("Enter your value: ")
print(val, type(val))
Enter your value: Hello Bee Hello Bee <class 'str'>

The classic beginner bug

age = input("Age: ")   # "30" the string
print(age + 1)
TypeError: can only concatenate str (not "int") to str

Fix: convert it yourself.

age = int(input("Age: "))
print(age + 1)   # 31

Printing to a file or stderr

import sys
print("went wrong", file=sys.stderr)

with open("log.txt", "w") as f:
    print("line one", file=f)
04 / Basics

Comments and Docstrings NOTES TO HUMANS

Comments are ignored by the interpreter. They exist for the next person to read the code, which is usually you in three months.

Single line — #

# This whole line is a comment
total = 42   # and this trailing part is too

Several lines

# Python has no /* */ block comment.
# You just stack # lines.
# Most editors do this with Cmd+/.

Triple-quoted strings

""" This is technically a string
expression, not a comment. Python
evaluates it and throws it away. """

name = "bee"
print(name)
bee

People use these as block comments. It works, but the real job of triple quotes is the docstring below.

Docstrings — the useful kind

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__

Write why, not what

# 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.

Never leave commented-out code

Delete it. Version control remembers it for you. Dead commented blocks rot and mislead readers.

05 / Values

Variables and Data Types NAMES POINT AT OBJECTS

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.

Binding and rebinding

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

The built-in types you need on day one

TypeLiteralNote
int50, -7, 1_000_000Unlimited size
float60.5, 2e-364-bit, inexact
complex3jReal + imaginary
boolTrue, FalseSubclass of int
str"bee"Immutable text
NoneTypeNone"No value"
list[1, 2]Mutable sequence
tuple(1, 2)Immutable sequence
dict{"k": 1}Key to value
set{1, 2}Unique, unordered

A name is a label, not a box

a b [1, 2, 3] one list object a = [1,2,3]; b = a → both point at the same list

This picture explains most surprises later in the deck: b = a copies the arrow, not the object.

Naming rules and conventions

ThingStyleExample
Variable / functionsnake_casemax_retries
ConstantUPPER_SNAKEDEFAULT_TIMEOUT
ClassPascalCaseDataProcessor
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.

06 / Values

Type Checking and Conversion ASK, THEN CAST

Python is dynamically typed (a name can hold anything) but strongly typed (it will not silently add a string to a number for you).

What am I holding?

x = 50
print(type(x))              # <class 'int'>

# Prefer isinstance for checks;
# it respects inheritance.
isinstance(x, int)          # True
isinstance(x, (int, float))  # True
<class 'int'>

Converting

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

Conversion can fail

int("twelve")
ValueError: invalid literal for int() with base 10: 'twelve'

Wrap user input in try / except ValueError. See the errors slide.

Truthiness — what counts as False

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.

Type hints — optional, but read them

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.

07 / Values

Operators SYMBOLS THAT DO WORK

Operators act on values and variables. Six families cover almost everything you will write.

Arithmetic

+add7 + 2 = 9
-subtract7 - 2 = 5
*multiply7 * 2 = 14
/true divide7 / 2 = 3.5
//floor divide7 // 2 = 3
%remainder7 % 2 = 1
**power7 ** 2 = 49

/ always gives a float, even 4 / 2 = 2.0.

Assignment

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)

Comparison → bool

==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

Logical

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":
    ...

Membership and identity

"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.

Bitwise (on ints)

&AND6 & 3 = 2
|OR6 | 3 = 7
^XOR6 ^ 3 = 5
~NOT~6 = -7
<<shift left6 << 1 = 12
>>shift right6 >> 1 = 3

Precedence

** then unary - then * / // % then + - then comparisons then not then and then or. When in doubt, add parentheses. Nobody has ever complained about clear grouping.

08 / Strings

Strings: Create, Index, Slice IMMUTABLE TEXT

A string is an ordered sequence of characters. Once created it cannot be changed. Every "modification" returns a new string.

Creating

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'

Indexing — forwards and backwards

s = "BeePostive"
s[0]     # 'B'   first
s[3]     # 'P'
s[-1]    # 'e'   last
s[-2]    # 'v'   second last
len(s)   # 10
s[99]    # IndexError

Immutability

s = "bee"
s[0] = "B"   # TypeError!

# Build a new one instead
s = "B" + s[1:]      # 'Bee'
s = s.replace("b", "B")  # 'Bee'

Slicing — s[start:stop:step]

B e e P o s t 0 1 2 3 4 5 6 -7 -6 -5 -4 -3 -2 -1 s[2:5] start is included, stop is excluded

Every slice form

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.

09 / Strings

String Methods and f-strings THE DAILY TOOLKIT

Methods never modify the original. They return a new string, so you must assign the result.

The methods you will actually use

CallResult
"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'

Assign the result

s = "  bee  "
s.strip()          # computed, thrown away
print(s)           # still '  bee  '

s = s.strip()      # correct

f-strings — how you format text now

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=}")
Hi bee, you scored 93.4567 Rounded: 93.46 Padded: bee| Percent: 87.6% Commas: 1,234,567 Expression: 187 Debug: score=93.4567

The older styles you will still meet

# .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.

10 / Control Flow

Conditional Statements if / elif / else

Run different blocks depending on a condition. The colon opens the block, the indentation defines it.

The full form

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")
x is greater than 5

Only the first matching branch runs. elif can repeat any number of times. else is optional.

Nesting and how to avoid it

# 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()

Truthiness in conditions

items = []

if items:                 # idiomatic
    print("has items")
else:
    print("empty")

# Not this
if len(items) > 0: ...
if items != []: ...
empty

The one-line conditional expression

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 later

match 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.

11 / Control Flow

Loops for AND while

Use for when you know what you are iterating over. Use while when you are waiting for a condition to change.

for — walk a sequence

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(start, stop, step)

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)))
[0, 1, 2, 3, 4]

stop is excluded, same rule as slicing. range is lazy: it generates numbers on demand, so range(10**9) uses no memory.

while — repeat until false

i = 0
while i < 5:
    print(i)
    i += 1            # do not forget this
0 1 2 3 4

Forget the increment and you have an infinite loop. Ctrl+C stops it.

The read-until-done pattern

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.

Nested loops

for row in range(3):
    for col in range(3):
        print(row, col, end="  ")
    print()

A break exits only the innermost loop.

12 / Control Flow

Loop Control and Helpers break, continue, enumerate, zip

break exits, continue skips

for i in range(10):
    if i == 5:
        break       # leave the loop entirely
    if i == 3:
        continue    # jump to next iteration
    print(i)
0 1 2 4

3 is skipped by continue. 5 onward never runs because break fired.

else on a loop — the surprising one

for n in [1, 3, 5]:
    if n % 2 == 0:
        print("found an even")
        break
else:
    print("no even numbers at all")
no even numbers at all

The else runs only if the loop finished without hitting break. Read it as "no break".

enumerate — index and value together

vowels = ['a', 'e', 'i', 'o', 'u']

for i, letter in enumerate(vowels):
    print(i, letter)
0 a 1 e 2 i 3 o 4 u
# 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.

zip — walk two sequences in step

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')]

Other iteration helpers

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
13 / Collections

Lists ORDERED, MUTABLE, DUPLICATES OK

The workhorse collection. An ordered sequence you can grow, shrink, and rearrange in place.

Create and read

var = ["Bee", "Post", "ive"]
print(var)
['Bee', 'Post', 'ive']
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

Change in place

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

sort() vs sorted() — the trap

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.

Copying — the other trap

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)

Useful patterns

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)
14 / Collections

Tuples ORDERED, IMMUTABLE

A tuple is a list that cannot change after creation. Use it for a fixed group of values that belong together.

Create and read

var = ("Bee", "Post", "ive")
print(var)
('Bee', 'Post', 'ive')
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

The one-element gotcha

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)

Packing and unpacking

# 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

Functions return tuples

def min_max(values):
    return min(values), max(values)

lo, hi = min_max([4, 9, 1])
print(lo, hi)
1 9

"Multiple return values" in Python is really one tuple, packed on the way out and unpacked on the way in.

Why choose a tuple

  • It signals "this will not change" to every reader
  • Slightly smaller and faster than a list
  • Hashable, so it can be a dict key or a set member. A list cannot.
grid = {(0, 0): "origin", (1, 2): "target"}
15 / Collections

Dictionaries KEY → VALUE

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.

Create and read

d = {1: 'Bee', 2: 'For', 3: 'Bee'}
print(d)
{1: 'Bee', 2: 'For', 3: 'Bee'}
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

Add, update, delete

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

Iterating — three views

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)
a = 1 b = 2
list(d.keys())     # ['a', 'b']
sorted(d)          # ['a', 'b']  sorts keys

Keys must be hashable

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.

Two facts worth knowing

  • Insertion ordered since 3.7. Iterating gives you keys in the order you added them. Before 3.7 you needed OrderedDict.
  • Duplicate keys collapse. {"a": 1, "a": 2} is {"a": 2}. The last write wins.
16 / Collections

Sets UNIQUE, UNORDERED

A bag of distinct items with no order. Built for two questions: "have I seen this?" and "what do these two groups share?"

Create

var = {"Bee", "For", "Bee"}
print(var)
{'For', 'Bee'}

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'}

The empty set trap

x = {}          # this is an empty DICT
type(x)         # <class 'dict'>

x = set()       # this is an empty SET

Change

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

Set algebra

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
a - b a & b b - a

Where sets earn their keep

# 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.

17 / Collections

Choosing the Right Collection ONE TABLE, FOUR TYPES

Most beginner code reaches for a list every time. Picking correctly makes code both faster and clearer about intent.

TypeLiteralOrderedMutableDuplicatesLookup by valueReach for it when
list[1, 2] YesYesYesSlow (scan) A sequence you will append to, sort, or index
tuple(1, 2) YesNoYesSlow (scan) A fixed record, or a dict key
dict{"k": 1} Yes (insertion)YesKeys uniqueFast by key You look things up by a name or id
set{1, 2} NoYesNoFast Uniqueness, or "have I seen this?"

Quick decision path

  • Need a label for each value? → dict
  • Only care about uniqueness? → set
  • Must never change? → tuple
  • Otherwise → list

They convert freely

list({1, 2})        # set  → list
set([1, 1, 2])     # list → set
tuple([1, 2])      # list → tuple
list(d.items())     # dict → pairs
dict(pairs)         # pairs → dict

Worth graduating to

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
18 / Collections

Comprehensions BUILD A COLLECTION IN ONE LINE

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.

From loop to comprehension

# 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)
[0, 1, 4, 9, 16]

Shape: [ expression for item in iterable ]

Add a filter

# 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)]

Nested

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]

All four flavours

# 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)
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
# GENERATOR — parentheses, lazy
gen = (n * n for n in range(1_000_000))
sum(gen)   # computes one at a time,
           # never builds the list

Know when to stop

# 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.

19 / Functions

Functions NAME A PIECE OF WORK

A function groups statements that serve one purpose, so you can run them on different inputs instead of copying the code.

Define and call

# A simple Python function
def fun():
    print("Welcome to BeePostive")

# Nothing happens until you call it
fun()
Welcome to BeePostive

def name ( params ) colon, then an indented body. Define before you call.

Parameters

# 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)
even odd

return ends the function

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)}")
add is 5 is_true is True

Every function returns something

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.

Returning several values

def func():
    return 1, 2, 3, 4, 5

one, two, three, four, five = func()
print(one, two, three, four, five)
1 2 3 4 5

The values are packed into a tuple on the way out and unpacked on the way in.

Functions are objects too

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"])

Good shape for a function

  • Does one thing, and the name says what
  • Under about 30 lines
  • Returns a value rather than mutating its arguments
  • Has a docstring if anyone else will call it
20 / Functions

Function Arguments *args AND **kwargs

Arguments are the values you pass between the parentheses. Python gives you several ways to accept them, from strict to wide open.

Positional, keyword, default

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

Force keywords with a bare *

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).

Unpacking at the call site

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
arg1: Bee arg2: for arg3: Bee arg1: Bee arg2: for arg3: Bee

*args — any number of positionals

def 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 named

def show(**kwargs):
    print(type(kwargs))  # <class 'dict'>
    for k, v in kwargs.items():
        print(k, "=", v)

show(host="db", port=5432)
host = db port = 5432

The names args and kwargs are convention. The * and ** are what matter.

The full ordering

def f(pos, /, normal, *args, kwonly, **kwargs):
    ...
posbefore /: positional only
normalpositional or keyword
*argsextra positionals → tuple
kwonlyafter *: keyword only
**kwargsextra keywords → dict

You rarely need all five. Recognising them when you read library code is enough.

21 / Functions

Scope and Mutability WHERE THE BUGS LIVE

Two ideas explain most confusing Python behaviour: where a name is visible, and whether the object behind it can change.

Scope: names are local by default

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

Reading is fine, assigning is the trigger

LIMIT = 10

def check(n):
    return n < LIMIT   # reads global, fine

Trap 1: the mutable default argument

# 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

Trap 2: arguments are shared references

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.

22 / Built-ins

Built-in Functions ALWAYS AVAILABLE, NO IMPORT

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.

Size, math, rounding

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)

Constructors

int float str boolscalars
list tuple dict setcollections
range(5)lazy numbers

Iterating and reshaping

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)

Inspection and I/O

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

Prefer the built-in

# 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.

23 / Errors

Errors and Exceptions try / except / raise

When something goes wrong Python raises an exception. Unhandled, it stops the program and prints a traceback. Read tracebacks from the bottom up.

Anatomy of a traceback

Traceback (most recent call last): File "app.py", line 12, in <module> main() File "app.py", line 8, in main total = price / count ZeroDivisionError: division by zero

Last line = what went wrong. Line above it = where. Everything above that is how you got there.

Exceptions you will meet first

SyntaxErrorCode will not even parse
IndentationErrorBad whitespace
NameErrorUndefined name, often a typo
TypeErrorWrong type for the operation
ValueErrorRight type, bad value
IndexErrorList index out of range
KeyErrorDict key missing
AttributeErrorNo such method or field
FileNotFoundErrorBad path
ZeroDivisionErrorDivided by 0

Handling

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
trycode that might fail
exceptruns only on that error
elseruns if nothing failed
finallyruns either way
# Catch several, capture the object
except (ValueError, TypeError) as err:
    print(f"failed: {err}")

Never do this

try:
    risky()
except:          # catches EVERYTHING,
    pass         # then hides it. Undebuggable.

Catch the specific exception you can actually handle, and at minimum log the rest.

Raising your own

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
24 / Files

Reading and Writing Files ALWAYS USE with

Open a file, use it, close it. The with statement closes it for you, even if an exception is raised in the middle.

The one pattern to remember

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.

Three ways to read

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.

Writing

# "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")

Modes

ModeMeansIf missing
"r"read (default)Error
"w"write, erase firstCreated
"a"append at endCreated
"x"create onlyCreated
"rb" "wb"binary bytes

Opening with "w" deletes the existing contents immediately. Use "a" when you meant to add.

Paths — use pathlib

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)

JSON — the format you will hit first

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
25 / Modules

Modules, Imports, and the Standard Library BATTERIES INCLUDED

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 forms

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.

Your own module

# 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 order (PEP 8)

import json                   # 1. standard library
from pathlib import Path

import requests               # 2. third party

from myapp.utils import greet  # 3. your code

Standard library worth knowing on day one

ModuleFor
mathsqrt, floor, pi, log
randomchoice, randint, shuffle
datetimeDates, times, differences
jsonRead and write JSON
pathlibFile paths done properly
os, sysEnvironment, argv, exit
collectionsCounter, defaultdict, deque
itertoolschain, groupby, combinations
reRegular expressions
csvComma separated files
loggingReal logging, not print
unittestBuilt-in test framework
secretsTokens and keys, not random

Small tastes

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)]

Third party packages

(.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.

26 / OOP

Classes and Objects DATA + BEHAVIOUR TOGETHER

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.

Define a class

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})"

Make and use objects

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

What is 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.

One blueprint, many objects

class Dog blueprint: name, age, speak() a 'Rex', 3 b 'Ada', 5 c 'Bo', 1 Dog(...) → instances

Dunder methods — hooks into syntax

__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.

For plain data, use a dataclass

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.

27 / OOP

The Four Pillars ENCAPSULATION, INHERITANCE, POLYMORPHISM, ABSTRACTION

1. Encapsulation — control what is reachable

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.

The pythonic getter: @property

class Car:
    @property
    def make(self):
        return self._make

c.make        # no parentheses, reads like a field

2. Inheritance — reuse and specialise

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

3. Polymorphism — same call, different behaviour

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.

4. Abstraction — publish the what, hide the how

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.

28 / Gotchas

Ten Traps, Up Front SAVE YOURSELF AN AFTERNOON

Every one of these bites a new Python programmer at least once. Read them now and recognise them later.

1. Mutable default argument

def f(x, acc=[]):   #  shared!
def f(x, acc=None): #  fix

2. b = a is not a copy

b = a          # same list object
b = a.copy()   # actual copy
b = copy.deepcopy(a)  # nested

3. Mutating while iterating

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)]

4. is vs ==

a = [1]; b = [1]
a == b   # True   same value
a is b   # False  different objects

Use is only with None, True, False.

5. Float arithmetic is inexact

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

6. / always gives a float

4 / 2     # 2.0  not 2
7 // 2    # 3    integer division
-7 // 2   # -4   floors, not truncates

7. In-place methods return None

x = x.sort()      # x is now None
x.sort()          # correct
y = sorted(x)     # or this

8. {} is a dict, not a set

empty_dict = {}
empty_set  = set()

9. Building strings in a loop

# O(n^2): new string every time
s = ""
for w in words:
    s += w

# O(n): the right way
s = "".join(words)

10. Shadowing a built-in

list = [1, 2]      # now list() is broken
list("abc")        # TypeError

Also watch str, dict, id, sum, type, input, next.

Debugging moves that always help

  • print(f"{x=} {type(x)=}")
  • Read the traceback bottom line first
  • breakpoint() drops you into a debugger
  • Reproduce it in the REPL on the smallest input
  • Check indentation before anything clever
29 / Wrap Up

What You Can Do Now, and What Is Next

You can now

  • Run Python from the REPL and from a file, inside a venv
  • Use every core type: int, str, list, dict, tuple, set
  • Branch and loop, and pick the right loop helper
  • Write functions with defaults, *args, and **kwargs
  • Handle errors instead of crashing
  • Read and write files and JSON safely
  • Split code into modules and import them
  • Read and write a class, and explain the four pillars

PEP 8, the style guide everyone follows

  • 4 spaces per indent level, never tabs
  • Lines under about 88 characters
  • snake_case for functions and variables, PascalCase for classes
  • Two blank lines between top level definitions
  • Let a formatter do it: pip install ruff, then ruff format . and ruff check .

Learn next, roughly in this order

TopicWhy it matters
Type hints + mypyCatch whole classes of bugs before running
pytestTests are how you change code without fear
Generators, yieldProcess data larger than memory
DecoratorsYou will meet @property, @cache, framework routes
loggingReplaces print in anything real
itertools, functoolsThe rest of the standard toolkit
asyncioConcurrency for I/O bound work
Packagingpyproject.toml, publishing, layouts

Practice beats reading

  • Count word frequency in a text file (dict or Counter)
  • Read a CSV and print a per-column summary
  • A command line to-do list backed by JSON
  • FizzBuzz, then rewrite it three different ways
  • Take any loop you wrote today and turn it into a comprehension, then decide which reads better

Questions?

Open a REPL and try the thing you are unsure about. That is the whole method.