Lectures: Nguyễn Tuấn Phong, Đào Việt Anh
Labs: Trịnh Ngọc Huỳnh, Phạm Tiến Du
Semester: 2025-2026-1
Institute for AI, VNU-UET
# decomposition: small reusable part (function)
def add(a, b):
return a + b
# abstraction: hide details from users
def calculator():
print(add(2, 3))
def is_even(i):
"""Check if an integer is even.
Args:
i (int): integer to check
Returns:
bool: True if even, False if odd
"""
print("inside is_even")
return i % 2 == 0
# Call the function
is_even(3)
x = 10 # global
def show():
y = 5 # local
print(x, y)
show() # works: 10 5
print(x) # works: 10
print(y) # error (not in scope)
A more sophisticated example of scope:
a = 5 # global
def g(x): # global function g, parameter x (local to g)
def h(): # nested function h, local to g
x = 'abc' # local to h
x = x + 1 # local to g
print('g: x =', x)
h()
return x
x = 3 # global
z = g(x) # global
→ Run in Notebook
>>> def is_even(i):
... print(i % 2 == 0) # prints result
...
>>> result = is_even(3)
False
>>> print(result)
None
| return | ||
|---|---|---|
| Where used | Only inside functions | Works outside functions too |
| Count | Only one return per call | Can have many print statements |
| Execution | Code after return not executed | Code continues after print |
| Value | Passed back to caller | Output shown on console |
Provide default values so callers can omit some arguments
def greet(name, msg="Hello"):
print(msg, name)
>>> i = 5
>>> def f(arg=i):
... print(arg)
...
>>> i = 6
>>> f()
5
>>> def f(a, L=[]):
... L.append(a)
... return L
...
>>> print(f(1))
[1]
>>> print(f(2))
[1, 2]
>>> print(f(3))
[1, 2, 3]
def f(a, L=None):
if L is None:
L = []
L.append(a)
return L
>>> def greet(name, msg="Hello", punctuation="!"):
... print(msg, name + punctuation)
>>> greet("Alice")
Hello Alice!
>>> greet("Bob", msg="Hi")
Hi Bob!
>>> greet("Carol", punctuation=".")
Hello Carol.
>>> greet("Dave", punctuation=".", msg="Goodbye")
Goodbye Dave.
def greet(name, msg="Hello", punctuation="!"):
print(msg, name + punctuation)
greet("Alice") # 1 positional
greet(name="Bob") # 1 keyword
greet(name="Carol", msg="Hi") # 2 keywords
greet(msg="Hey", name="Dan") # order doesn’t matter
greet("Eve", "Welcome") # 2 positional
greet("Frank", punctuation=".") # mix positional + keyword
def greet(name, msg="Hello", punctuation="!"):
print(msg, name + punctuation)
greet() # missing required arg 'name'
greet(name="Alice", "Hi") # positional after keyword
greet("Bob", name="Bob") # duplicate value for 'name'
greet(person="Eve") # unknown keyword 'person'
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
----------- ---------- ----------
| | |
| Positional or keyword |
| - Keyword only
-- Positional only
def standard_arg(arg):
print(arg)
def pos_only_arg(arg, /):
print(arg)
def kwd_only_arg(*, arg):
print(arg)
def combined_example(pos_only, /, standard, *, kwd_only):
print(pos_only, standard, kwd_only)
standard_arg(2)
standard_arg(arg=2)
pos_only_arg(1)
pos_only_arg(arg=1) # TypeError
kwd_only_arg(3) # TypeError
kwd_only_arg(arg=3)
combined_example(1, 2, 3) # TypeError
combined_example(1, 2, kwd_only=3) # OK
combined_example(1, standard=2, kwd_only=3) # OK
combined_example(pos_only=1, standard=2, kwd_only=3) # TypeError
>>> def demo(x, *args, **kwargs):
... print("x:", x)
... print("args:", args)
... print("kwargs:", kwargs)
...
>>> demo(1, 2, 3, a=10, b=20)
x: 1
args: (2, 3)
kwargs: {'a': 10, 'b': 20}
>>> demo(1)
x: 1
args: ()
kwargs: {}
>>> demo(1, 2, 3)
x: 1
args: (2, 3)
kwargs: {}
>>> demo(1, a=10)
x: 1
args: ()
kwargs: {'a': 10}
Note: Both *args and **kwargs can be used to pass a variable number of arguments (0 or more) to a function.
Params after *args are keyword-only
>>> def concat(*args, sep="/"):
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'
>>> def f(*args, x):
... print(args, x)
...
Question: Given the function definition above, are the following function calls valid?
Positional-only names can appear in **kwds keys
Question: Explain the difference:
>>> def foo(name, **kwds):
... return 'name' in kwds
...
>>> foo(1, name=2)
TypeError: foo() got multiple values for argument 'name'
>>> def foo(name, /, **kwds):
... return 'name' in kwds
...
>>> foo(1, name=2)
True
>>> def add(a, b, c):
... print(a + b + c)
>>> nums = [1, 2, 3]
>>> add(*nums)
6
>>> def greet(name, msg="Hi"):
... print(msg, name)
>>> opts = {"name": "Alice", "msg": "Hello"}
>>> greet(**opts)
Hello Alice
>>> f = lambda x, y: x + y
>>> f(1, 2)
3
>>> make_inc = lambda n: (lambda x: x + n)
>>> f = make_inc(10)
>>> f(5)
15
>>> nums = [5, 2, 9, 1]
>>> sorted(nums, key=lambda x: -x)
[9, 5, 2, 1]
>>> words = ["apple", "banana", "kiwi"]
>>> sorted(words, key=lambda w: len(w))
['kiwi', 'apple', 'banana']
>>> def my_function():
... """Do nothing, but document it.
...
... No, really, it doesn't do anything.
... """
... pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.
No, really, it doesn't do anything.
Create file fibo.py = Create a "fibo" module
# fibo.py: The Fibonacci module
def seq(n):
a, b, out = 0, 1, []
while a < n:
out.append(a)
a, b = b, a + b
return out
Use the module in another script or interactive session
>>> import fibo # import the module
>>> fibo.seq(20)
[0, 1, 1, 2, 3, 5, 8, 13]
>>> from fibo import seq as fib # import funtion directly, rename
>>> fib(50)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# demo.py
print("Module is being imported")
def hello():
print("Hello from demo")
>>> import demo
Module is being imported
>>> import demo # nothing happens this time
>>> demo.hello()
Hello from demo
# fibo.py
def fib(n):
pass
def fib2(n):
pass
Import specific functions:
>>> from fibo import fib, fib2
>>> fib(500)
Import everything (⚠️ not recommended):
>>> from fibo import *
>>> fib(500)
Import module under an alias:
>>> import fibo as fib
>>> fib.fib(500)
Import function under an alias:
>>> from fibo import fib as fibonacci
>>> fibonacci(500)
$ python module.py args
# fibo.py
def fib(n):
a, b = 0, 1
while a < n:
print(a, end=" ")
a, b = b, a + b
print()
# This block runs only if the module is executed directly (via command line)
if __name__ == "__main__":
import sys
fib(int(sys.argv[1])) # sys.argv[1] is the first command-line argument
Run the script with a command-line argument:
$ python fibo.py 50
0 1 1 2 3 5 8 13 21 34
Importing the module does not run the script portion:
>>> import fibo # importing does not run fib()
>>> fibo.fib(10)
0 1 1 2 3 5 8
>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!
>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir()
['__builtins__', '__name__', 'a', 'fib', 'fibo', 'sys']
shapes/ Top-level package
__init__.py
utils.py
geometry/ Subpackage
__init__.py
circle.py
square.py
# Full path
import shapes.geometry.circle
shapes.geometry.circle.area(5)
# From subpackage
from shapes.geometry import square
square.perimeter(4)
# Direct function
from shapes.geometry.circle import area
area(5)
# shapes/geometry/__init__.py
__all__ = ["circle", "square"]
# main.py
from shapes.geometry import *
print(circle.area(5))
print(square.perimeter(4))
‼️ Best practice: avoid import * in production code
shapes/
__init__.py
utils.py
geometry/
__init__.py
circle.py
square.py
Absolute import (full path):
from shapes.geometry import circle
Relative import (use leading dots):
from . import circle # current package
from .. import utils # parent package
from ..geometry import square # module from parent package
>>> with open("workfile", "w", encoding="utf-8") as f:
... f.write("This is a test.\n")
...
16
>>> with open("workfile", encoding="utf-8") as f:
... read_data = f.read()
...
>>> read_data
'This is a test.\n'
>>> with open("workfile", encoding="utf-8") as f:
... read_data = f.read()
...
>>> # Check that the file has been automatically closed.
>>> f.closed
True
>>> f.close()
>>> f.read()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.
>>> with open("workfile", "w", encoding="utf-8") as f:
... _ = f.write("Hello\nWorld\n")
...
>>> with open("workfile", "r", encoding="utf-8") as f:
... f.read()
...
'Hello\nWorld\n'
>>> with open("workfile", "r", encoding="utf-8") as f:
... f.read(5)
...
'Hello'
>>> with open("workfile", "r", encoding="utf-8") as f:
... f.readline()
...
'Hello\n'
>>> with open("workfile", "r", encoding="utf-8") as f:
... f.readline(); f.readline(); f.readline()
...
'Hello\n'
'World\n'
''
Use a for loop to read lines one by one:
>>> with open("workfile", "r", encoding="utf-8") as f:
... for line in f:
... print(line.strip())
Hello
World
>>> with open("log.txt", "w", encoding="utf-8") as f:
... n = f.write("This is a test.\n")
...
>>> n
16
>>> with open("workfile", encoding="utf-8") as f:
... f.read(4)
... f.tell()
...
'This'
4
>>> with open("workfile", "w", encoding="utf-8") as f:
... _ = f.write("Hello\nWorld\n")
...
>>> with open("workfile", "r", encoding="utf-8") as f:
... f.seek(0) # Go to beginning
... f.read(5)
... f.seek(0, 2) # Go to end
... f.read()
...
0
'This '
16
''
To/From Strings
>>> import json
>>> data = {"name": "Alice", "age": 30, "is_member": True}
>>> # Serialize a dict to JSON string
>>> s = json.dumps(data)
>>> s
'{"name": "Alice", "age": 30, "is_member": true}'
>>> # Deserialize JSON string back to dict
>>> json.loads(s)
{'name': 'Alice', 'age': 30, 'is_member': True}
To/From Files
>>> with open("data.json", "w", encoding="utf-8") as f:
... json.dump(data, f)
>>> with open("data.json", "r", encoding="utf-8") as f:
... loaded = json.load(f)
>>> loaded
{'name': 'Alice', 'age': 30, 'is_member': True}
>>> import csv
>>> rows = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
>>> with open("data.csv", "w", encoding="utf-8") as f:
... writer = csv.DictWriter(f, fieldnames=rows[0].keys())
... writer.writeheader()
... writer.writerows(rows)
The created data.csv file contains:
name,age
Alice,30
Bob,25
>>> import csv
>>> with open("data.csv", encoding="utf-8") as f:
... reader = csv.DictReader(f)
... for row in reader:
... print(row)
...
{'name': 'Alice', 'age': '30'}
{'name': 'Bob', 'age': '25'}
>>> year, event = 2025, "IAI Connect"
>>> f"Summary of the {year} {event}"
'Summary of the 2025 IAI Connect'
>>> import math
>>> f"pi ≈ {math.pi:.3f}"
'pi ≈ 3.142'
>>> tbl = {"Sjoerd": 4127, "Jack": 4098}
>>> for name, phone in tbl.items():
... print(f"{name:10} => {phone:8d}")
Sjoerd => 4127
Jack => 4098
>>> animals = "eels"
>>> f"My hovercraft is full of {animals!r}."
"My hovercraft is full of 'eels'."
>>> f"My hovercraft is full of {animals!s}."
'My hovercraft is full of eels.'
>>> bugs, count = "roaches", 13
>>> f"{bugs=}, {count=}"
"bugs='roaches', count=13"
>>> "We are the {} who say '{}'!".format("knights", "Ni")
'We are the knights who say "Ni"!'
>>> "{0} and {1}; {1} and {0}".format("spam", "eggs")
'spam and eggs; eggs and spam'
>>> "This {food} is {adjective}.".format(food="spam", adjective="awful")
'This spam is awful.'
>>> table = {"Jack": 4098, "Sjoerd": 4127, "Dcab": 8637678}
>>> "Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}".format(**table)
'Jack: 4098; Sjoerd: 4127; Dcab: 8637678'
>>> for x in range(1, 6):
... print("{0:2d} {1:3d} {2:4d}".format(x, x*x, x*x*x))
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
>>> # Right-justify columns (same table as previous slide's)
>>> for x in range(1, 6):
... left = repr(x).rjust(2)
... mid = repr(x*x).rjust(3)
... right = repr(x*x*x).rjust(4)
... print(left, mid, right)
...
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
>>> # Pad with zeros
>>> "12".zfill(5), "-3.14".zfill(7)
('00012', '-003.14')
>>> import math
>>> "pi ≈ %5.3f" % math.pi
'pi ≈ 3.142'