Lesson 03

Functions and collections

Code organization and key-value data structures

Previously

Lesson 2 - loops and lists

  • while and for + range()
  • break and continue
  • Lists and indexing
  • Slicing and steps
  • List methods
  • Iterating collections

Lesson 1 recap

Variables and types

Every value has a type. Python infers it automatically.

name = "Alice" age = 30 price = 19.99 is_admin = True print(type(age)) print(type(is_admin))
<class 'int'> <class 'bool'>

Lesson 1 recap

Input and conversion

input() always returns a string. For numbers you need int() or float().

# user types: 5 n = int(input("Number: ")) print(n * 2)
Number: 5 10

Lesson 1 recap

Conditions if / elif / else

Different branches for different cases. An f-string embeds values into text.

n = 5 if n > 0: print(f"{n}: positive") elif n < 0: print(f"{n}: negative") else: print("zero")
5: positive

Lesson 2 recap

The for loop and range

Repeat a known number of times. range(1, 6) yields 1..5 (no 6).

for i in range(1, 6): print(i, end=" ")
1 2 3 4 5

Real world

The for loop in practice

Not a toy example: a loop polls a health endpoint until the service comes back up after deploy.

for-loop polling health endpoint

for i in 1 2 3 4 5; do ... done - this is what monitoring, retry logic, migration tests and hundreds of other things are built on.

Lesson 2 recap

Lists and slicing

Ordered collection. Zero-based indexing. Slicing with a colon.

fruits = ["apple", "pear", "plum", "cherry"] print(fruits[0]) print(fruits[-1]) print(fruits[1:3])
apple cherry ['pear', 'plum']

Lesson 2 recap

break and continue

continue: skip this iteration. break: exit the loop.

for x in range(10): if x == 3: continue if x == 7: break print(x, end=" ")
0 1 2 4 5 6

Today

Functions and collections

tuples
sets
dicts
def · return
*args · **kwargs
scope · lambda

What is it?

Tuples

An ordered collection that cannot change after creation. Used for fixed groups: coordinates (x, y), RGB color, dates (year, month, day). Python returns a tuple when a function returns multiple values at once.

0"Moscow"
1"Tashkent"
2"Bishkek"
capitals = ("Moscow", "Tashkent", "Bishkek") point = (10, 20) single = (42,) # comma is required!

Why?

Tuple vs list

If lists exist, why use tuples? Immutability isn't a weakness - it's protection.

List · []

Mutable

  • • can add / remove items
  • • fits growing data
  • • slightly slower
Tuple · ()

Immutable

  • • safe from accidental edits
  • • can be a dict key, set item
  • • faster, lighter
Rule of thumb

Fixed groups (coordinates, RGB, dates) → tuple. Growing collections → list.

Example 1

Access by index

Predict the result. Press → to reveal.

capitals = ("Moscow", "Tashkent", "Bishkek") print(capitals[0])
Moscow

Indexing starts from zero, like a list. capitals[-1] is the last element.

Example 2

Tuple length

Predict the result. Press → to reveal.

capitals = ("Moscow", "Tashkent", "Bishkek") print(len(capitals))
3

len() works on any collection: tuple, list, set, dict, str.

Example 3

Trying to mutate

What happens if we try to reassign an element? Press → to reveal.

capitals = ("Moscow", "Tashkent", "Bishkek") capitals[0] = "X"
TypeError: 'tuple' object does not support item assignment

This is a feature, not a bug: tuples can't be accidentally broken. Need a different value? Build a new tuple.

What is it?

Sets

A structure from math: a collection of unique elements with no defined order. Python silently drops duplicates. The superpower is x in set - O(1) membership, instant even across millions of items.

Set visualization
tags = {"python", "sql", "python", "git"} empty = set() # caution: {} is a dict!

Motivation

Why we need sets

Fast lookup · dedup · set algebra

01

Deduplication

Strip duplicates in one line: list(set(items)).

02

Fast lookup

x in set - O(1), not O(n) like a list.

03

Set algebra

Intersection, union, difference - one line instead of nested loops.

# real: A/B test - who saw both old and new versions? test_a = set(users_in_old_flow) # 50,000 IDs test_b = set(users_in_new_flow) # 50,000 IDs overlap = test_a & test_b # intersection only_in_b = test_b - test_a # pure new print(f"Saw both versions: {len(overlap)}") print(f"Only new: {len(only_in_b)}")
Saw both versions: 1247 Only new: 48753

Example 1

Automatic deduplication

What does Python do with the duplicate "python"? Press → to reveal.

tags = {"python", "sql", "python", "git"} print(tags)
{'python', 'sql', 'git'}

The second "python" is gone - the set silently dropped the duplicate. Order isn't guaranteed.

Example 2

Length after dedup

We wrote 4 elements - how many end up in the set? Press → to reveal.

tags = {"python", "sql", "python", "git"} print(len(tags))
3

Not 4 but 3 - sets count uniques only. A clean way to answer "how many distinct values are in this list?"

Example 3

Membership test

What does x in set return? Press → to reveal.

tags = {"python", "sql", "git"} print("python" in tags) print("java" in tags)
True False

Lookup in a set is O(1). The same in on a list would be O(n) - huge difference on a million items.

Operations

Set algebra

Op Meaning Example
a | b Union {1,2,3} | {3,4} → {1,2,3,4}
a & b Intersection {1,2,3} & {2,3,4} → {2,3}
a - b Difference {1,2,3} - {2,3} → {1}
a ^ b Symmetric diff. {1,2,3} ^ {2,3,4} → {1,4}
Trick

De-duplicate a list with list(set(items)) - but order is lost.

What is it?

Dictionaries

A mapping from a key (name, ID, date) to a value (any object). Lookup by key runs in O(1). It's Python's workhorse collection: JSON responses, configs, caches, function parameters - all dicts under the hood.

keyvalue
"name"
"Alice"
"age"
30
"city"
"Tashkent"
user = {"name": "Alice", "age": 30, "city": "Tashkent"}

Motivation

Why we need dicts

Dict is Python's workhorse collection. JSON, configs, APIs, caches - all dicts under the hood.

01

Lookup by name

Phone book: name → number. O(1) instead of scanning a list.

02

JSON ↔ dict

API responses, configs, save files - all dicts in JSON wrappers.

03

Replace if-elif

10 if-elif branches → one d[key].

# real: parsing an API response response = requests.get("https://api.bank.uz/user/42").json() # response is already a dict name = response["full_name"] # required field email = response.get("email", "-") # may be missing balance = response.get("balance", 0) # default 0 print(f"{name}: {balance} sum")
Alisha Karimova: 1250000 sum

Example 1

Lookup by key

Predict the result. Press → to reveal.

user = {"name": "Alice", "age": 30} print(user["name"])
Alice

Square brackets with a key, like a list - but a name instead of a number: d["name"].

Example 2

How many pairs in a dict

Predict the result. Press → to reveal.

user = {"name": "Alice", "age": 30, "city": "Tashkent"} print(len(user))
3

len(d) is the number of pairs (key, value). Not "number of keys" or "number of values" - they're the same number.

Example 3

Key membership test

Does in check keys or values? Press → to reveal.

user = {"name": "Alice", "age": 30} print("age" in user) print("Alice" in user)
True False

Keys only! "Alice" is a value - search for it with in user.values().

Access

Read by key

user = {"name": "Alice", "age": 30}

Click an expression - see the result:

// click any expression
Watch out

d[k] on a missing key raises KeyError. .get() returns None or your default.

Iteration

Looping over a dict

scores = {"Anna": 85, "Bob": 72, "Vika": 91} for k in scores: print(k) # keys only for v in scores.values(): print(v) # values only for k, v in scores.items(): print(f"{k}: {v}")
Anna: 85 Bob: 72 Vika: 91

Question

Which collection fits best?

Store «city → population» pairs with fast lookup by city

Alist - ordered sequence
Btuple - immutable
Cset - unique items
Ddict - key-value pairs

Think for a minute →

Answer

D - dict

"City → population" is exactly the scenario dict was built for.

cities = { "Tashkent": 2_900_000, "Samarkand": 530_000, "Bukhara": 280_000, } print(cities["Tashkent"]) # O(1) - instant
2900000

list would search in O(n). set only holds values, with no association. tuple is immutable. dict is the only type actually built for key → value.

Cheat sheet

Collections - when to use which

Type Mutable Ordered Duplicates Main use case
list [] yes yes yes growing sequence
tuple () no yes yes fixed group
set {} yes no no unique + fast lookup
dict {k:v} yes yes (3.7+) keys unique lookup by name/key
Quick decision

"Lookup by name?" → dict. "Unique values, fast in?" → set. "Changes?" → list. "Doesn't change?" → tuple.

Halfway through

Break

5–10 minutes

☕ Coffee, stretch, questions - see you in the second half for functions

Motivation

Why we need functions

Without functions a 10,000-line program becomes hell. A function is a named block of code with its own world.

01

Reuse (DRY)

Describe logic once - call it a thousand times. Fix in one place - fixed everywhere.

02

Naming

Self-documenting code: calc_tax(...) beats 5 lines of formula.

03

Isolation

Own scope inside. Locals don't leak out. Outsiders can't break the internals.

# real example: discount-price calculation, # reused in cart, invoice, report, checkout... def final_price(price, discount=0, tax=0.12): return (price * (1 - discount)) * (1 + tax) cart_total = final_price(100, discount=0.10) invoice_sum = final_price(250) vip_offer = final_price(500, discount=0.25)
cart = 100.80 invoice = 280.00 vip = 420.00

Without vs with a function

One rule instead of 50 copies

Left: duplication hell. Right: reusable code.

without function

Each order is a copy-paste

# Order 1 p1 = 100 * 1.1 + 50 print(f"Order 1: {p1}") # Order 2 p2 = 250 * 1.1 + 50 print(f"Order 2: {p2}") # Order 3 p3 = 80 * 1.1 + 50 print(f"Order 3: {p3}") # ... 47 more orders
with function

Write once, call 50 times

def order_total(price): return price * 1.1 + 50 orders = [100, 250, 80, 500, 30] for i, p in enumerate(orders, 1): print(f"Order {i}: {order_total(p)}")
When the rule changes

Tax rate changed? Left side: 50 edits, easy to miss one. Right side: one line.

Syntax

Syntax: def

Defining a function - describing how to call it

def greet(name): """Greets a user.""" message = f"Hello, {name}!" return message # Call print(greet("Alice")) # Hello, Alice!
"Alice"
greet()
"Hello, Alice!"

Step by step

How a call works

Click "Run" - each step appears in turn

def square(n): result = n * n return result x = square(4)
Step Action Value
1call square(4)n = 4
2n * n16
3result = 16result = 16
4return result→ 16
5x = 16x = 16

return

Returning a value

def divmod_safe(a, b): if b == 0: return None return a // b, a % b q, r = divmod_safe(17, 5) print(q, r)
3 2
Remember

return ends the function. Multiple values come back as a tuple - you can unpack them into separate names.

Optional

Default parameters

Type a name and greeting - leave a field blank to use the default

def greet(name, greeting="Hello"): return f"{greeting}, {name}!"
Hello, Anna!

Defaulted parameters come after required ones in the signature.

Trap #1

Mutable default arguments

Default is evaluated once - at function definition, not on every call

dangerous

One list shared across calls

def add(item, cart=[]): cart.append(item) return cart print(add("apple")) # ["apple"] print(add("pear")) # ["apple", "pear"] ⚠
correct

New list every call

def add(item, cart=None): if cart is None: cart = [] cart.append(item) return cart
Analogy

"One shared notebook on the teacher's desk" vs "a fresh sheet for each student". Never use [], {}, set() as default values.

Positional

*args - variable positional

Drag the slider - watch arguments pack into a tuple

arguments: 3
call
123
↓ args
(1, 2, 3)
def total(*args): return sum(args) print(total(1, 2, 3))
6

Keyword

**kwargs - variable keyword

Add or remove fields - kwargs packs them into a dict

fields: 3
call
name="Anna" age=30 city="Tashkent"
↓ kwargs
{"name": "Anna", "age": 30, "city": "Tashkent"}
def profile(**kwargs): for k, v in kwargs.items(): print(f"{k}: {v}") profile(name="Anna", age=30, city="Tashkent")
name: Anna age: 30 city: Tashkent

Parameter order

Where does each value land?

Strict order: required → defaults → *args → keyword-only → **kwargs

def f(a, b=10, *args, status="active", **kwargs): print(a, b, args, status, kwargs) f(1, 2, 3, 4, status="pending", user="John")
a = 1 b = 2 # overrode the default args = (3, 4) status = "pending" kwargs = {"user": "John"}
Subtle point

Everything after *args is keyword-only. status can only be passed by name - otherwise the value would end up in args.

Unpacking at call site

f(*lst, **dct) - the same * and ** work in reverse: spread a list/dict into arguments.

Before diving in

Why do we need scope at all?

Imagine a 10,000-line project. If every variable were global, hell would start at line 10.

no scope

Name chaos

  • • Every function knows everything
  • • Can't reuse i, temp, result twice
  • • Change one - break another
  • • Nobody knows who mutates what
with scope

Isolated worlds

  • • Each function has its own namespace
  • i in f()i in g()
  • • Local changes stay local
  • • Easy to reuse and test
Analogy

Scope is a one-way mirror window. From inside a function you see the outer world (globals). From outside you have no access to local names.

Visibility

Scope

Click a name - Python looks it up via the LEGB rule

x = 100 # global
def demo():
    y = 5 # local
    print(x, y) # sees global x
demo() # 100 5
print(y) # try it: NameError
Click any variable name - see the lookup path

Rules

LEGB - name lookup order

Python searches names in this order

Level Where
L · Local Inside the current function
E · Enclosing In the outer function (if nested)
G · Global Module level
B · Built-in Built-in names: print, len, range...
Important

Assigning x = 5 inside a function creates a local variable, even if a global with the same name exists.

Writing to outer scope

global and nonlocal

Reading sees outer scope. Writing creates a local. To rebind an outer name you need the keyword.

global

write to module-level

counter = 0 def inc(): global counter counter += 1 inc(); inc() print(counter) # 2
nonlocal

write to enclosing function

def outer(): x = 0 def inner(): nonlocal x x += 1 inner(); inner() return x print(outer()) # 2
Without global

You get UnboundLocalError - Python treats the name as local the moment it sees an assignment. global explicitly says "don't create a local - write to module scope".

Concise

Lambda functions

A short, anonymous, single-expression function

via def

Multi-line

def double(x): return x * 2
via lambda

One line

double = lambda x: x * 2 double(5) # 10

Syntax: lambda args: expression. One expression only - no return keyword.

Line of good taste

When lambda becomes bad

Lambda is great for short expressions. If logic branches - write def. Code is written for humans.

horrible

Triple-nested ternaries

group = lambda x: "Adult" if x >= 18 else ("Teen" if x >= 13 else "Child")
readable

Plain function

def age_group(age): if age >= 18: return "Adult" if age >= 13: return "Teen" return "Child"
Rule of thumb

Lambda over ~50–60 chars or with conditionals - rewrite as def. A function name is free documentation.

Usage

lambda + sorted

Click a button - the cards physically rearrange

1Anna30
2Bob25
3Vika28
4Glen22
5Dima35
people # original
[ ('Anna', 30), ('Bob', 25), ('Vika', 28), ('Glen', 22), ('Dima', 35), ]

Trap #2

Late binding in lambda

A lambda captures the variable name, not its value at creation time

funcs = [lambda: i for i in range(3)] for f in funcs: print(f())
2 2 2 ⚠ not 0, 1, 2!

By the time we call the three lambdas the loop is done and i = 2. All three look at the same name.

# fix: "snapshot" the current i via a default argument funcs = [lambda i=i: i for i in range(3)] for f in funcs: print(f())
0 1 2

Question

What does this code print?

def f(x, y=10): return x + y print(f(5)) print(f(5, 20)) print(f(y=3, x=7))
A15 / 25 / 10
B15 / 25 / 21
C5 / 25 / Error
D15 / 20 / 10

Think for a minute →

Practice 1

Stats calculator

15:00
  1. 1 Write a function stats(*nums)
  2. 2 Return a dict: min, max, avg, count
  3. 3 If no args - return None
  4. 4 Test: stats(1, 2, 3, 4, 5)

Solution

Solution 1 walkthrough

def stats(*nums): if not nums: return None return { "min": min(nums), "max": max(nums), "avg": sum(nums) / len(nums), "count": len(nums), } print(stats(1, 2, 3, 4, 5)) # {"min": 1, "max": 5, "avg": 3.0, "count": 5}
*args guard dict-return

Practice 2

Top-3 students

15:00
scores = {"Anna": 85, "Bob": 72, "Vika": 91, "Glen": 68, "Dima": 88}
  1. 1 Write top_n(scores, n=3)
  2. 2 Use sorted + lambda by value
  3. 3 Return n best (name, score) pairs

Hint: scores.items() gives pairs; reverse=True for descending.

Solution

Solution 2 walkthrough

def top_n(scores, n=3): pairs = sorted( scores.items(), key=lambda p: p[1], reverse=True, ) return pairs[:n] scores = {"Anna": 85, "Bob": 72, "Vika": 91, "Glen": 68, "Dima": 88} print(top_n(scores)) # [("Vika", 91), ("Dima", 88), ("Anna", 85)]
items() sorted + lambda [:n]

Try it yourself

Live Python in the browser

Edit the code and click "Run" - it's real Python via Pyodide

Pyodide not loaded yet - click "Run"
// Output will appear here

Practice tasks

Jupyter Notebook

10 tasks covering today's topics. Open in Jupyter or VS Code and work through them.

.ipynb
lesson3_tasks.ipynb
10 practice tasks
Download EN Скачать RU Yuklab olish UZ

Wrap-up

Lesson summary

  • Tuples, sets, dicts
  • def, return, parameters
  • Mutable defaults trap
  • *args and **kwargs
  • LEGB · global · nonlocal
  • lambda + sorted/map/filter
  • Late binding in lambda
  • Unpacking f(*lst, **dct)
Next lesson

Modules, files and error handling - reading/writing data and try/except

Questions?

Telegram: @gokalqurt

Python · Lesson 3
RU UZ EN
1 / 59