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
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
^^^^
NameError: name 'spam' is not defined
while True:
try:
x = int(input("Please enter a number: "))
break
except ValueError:
print("Oops! That was no valid number. Try again...")
... except (RuntimeError, TypeError, NameError):
... pass
class B(Exception): pass
class C(B): pass
class D(C): pass
for cls in [B, C, D]:
try:
raise cls()
except D: # subclass first
print("D")
except C:
print("C")
except B: # base last
print("B")
# Prints: B, C, D
Question: What if we put except B first?
class B(Exception): pass
class C(B): pass
class D(C): pass
for cls in [B, C, D]:
try:
raise cls()
except B:
print("B")
except C:
print("C")
except D:
print("D")
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # the exception type
... print(inst.args) # arguments stored in .args
... print(inst) # __str__ allows args to be printed directly,
... # but may be overridden in exception subclasses
... x, y = inst.args # unpack args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
→ Errors not caught with OSError and ValueError will be caught by the generic Exception handler.
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()
>>> raise NameError('HiThere')
NameError: HiThere
>>> raise ValueError # same as ValueError()
>>> def square_root(x):
... if x < 0:
... raise ValueError("Cannot compute square root of negative number")
... return x ** 0.5
...
>>> square_root(-1)
ValueError: Cannot compute square root of negative number
class NegativeNumberError(Exception):
"""Raised when a negative number is not allowed."""
pass
def square_root(x):
if x < 0:
raise NegativeNumberError(f"Invalid input: {x}")
return x ** 0.5
print(square_root(-9))
# Raises: NegativeNumberError: Invalid input: -9
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
>>> def f():
... excs = [OSError('error 1'), SystemError('error 2')]
... raise ExceptionGroup('there were problems', excs)
...
>>> f()
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| f()
| ~^^
| File "<stdin>", line 3, in f
| raise ExceptionGroup('there were problems', excs)
| ExceptionGroup: there were problems (2 sub-exceptions)
+-+---------------- 1 ----------------
| OSError: error 1
+---------------- 2 ----------------
| SystemError: error 2
+------------------------------------
>>> try:
... f()
... except Exception as e:
... print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
excs = []
for test in tests:
try:
test.run()
except Exception as e:
excs.append(e)
if excs:
raise ExceptionGroup("Test Failures", excs)
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.add_note('Extra info 1')
... e.add_note('Extra info 2')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('We have some problems', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('operation failed')
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>> x = 5
>>> assert x > 0 # passes, no output
>>> assert x < 0 # AssertionError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
>>> def divide(a, b):
... assert b != 0, "Denominator must not be zero"
... return a / b
...
>>> print(divide(10, 2))
5.0
>>> print(divide(10, 0))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in divide
AssertionError: Denominator must not be zero
# ❌ Bad: input validation
assert user_age > 0, "Age must be positive"
# ✅ Better
if user_age <= 0:
raise ValueError("Age must be positive")
Simplest form of a class:
class ClassName:
<statement-1>
.
.
.
<statement-N>
Note: Definitions must be executed before use; can appear inside if or functions.
class Greeter:
msg = "Hello!"
# class object bound to Greeter
print(Greeter.msg) # access attribute directly from the class
Support: attribute reference and instantiation.
Attributes are accessed with obj.name.
>>> class MyClass:
... """A simple example class"""
... i = 12345
...
... def f(self):
... return 'hello world'
...
>>> MyClass.i
12345
>>> MyClass.f
<function MyClass.f at 0x7f9b8c2d1>
>>> MyClass.f() # missing instance argument
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() missing 1 required positional argument: 'self'
Call the class like a function to create an instance:
x = MyClass()
Customize initial state via __init__():
def __init__(self):
self.data = []
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
Rule: Args passed to class are forwarded to __init__.
Instances support attribute references only:
>>> class MyClass:
... """A simple example class"""
... i = 12345
...
... def f(self):
... return 'hello world'
...
>>> x = MyClass()
>>> x.counter = 1
>>> while x.counter < 10:
... x.counter = x.counter * 2
...
>>> print(x.counter)
16
>>> del x.counter
>>> x.f()
'hello world'
Methods are like functions, but bound to an instance:
>>> x = MyClass()
>>> x.f()
'hello world'
Methods can be stored and called later:
>>> xf = x.f
>>> while True:
... print(xf())
...
>>> class Dog:
... kind = 'canine' # class variable shared by all instances
... def __init__(self, name):
... self.name = name # instance variable unique to each instance
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
Avoid mutable class variables (e.g., lists) unless truly shared:
>>> class Dog:
... tricks = [] # mistaken use of a class variable
... def __init__(self, name):
... self.name = name
... def add_trick(self, trick):
... self.tricks.append(trick)
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unexpectedly shared by all dogs
['roll over', 'play dead']
>>> class Dog:
... def __init__(self, name):
... self.name = name
... self.tricks = [] # creates a new empty list for each dog
... def add_trick(self, trick):
... self.tricks.append(trick)
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
Same name on instance & class → instance wins.
>>> class Warehouse:
... purpose = 'storage'
... region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east
class Account:
def __init__(self, balance):
self._balance = balance # convention: internal
a = Account(100)
a._balance = -50 # possible, but breaks invariant
a.owner = "Alice" # allowed, adds new attribute
class Person:
def __init__(self, name):
self.name = name # must use self
def greet(self):
return "Hi, I'm " + self.name
p = Person("Bob")
print(p.greet()) # Hi, I'm Bob
Any function stored on a class becomes a method.
# Function defined outside the class
def f1(self, x, y):
return min(x, x+y)
class C:
f = f1
def g(self):
return 'hello world'
h = g
Call via self.method(...).
class Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
PI = 3.14 # module-level name
class Circle:
def __init__(self, r):
self.r = r
def area(self):
return PI * self.r * self.r # uses global PI
c = Circle(2)
print(c.area()) # 12.56
>>> 3 .__class__
<class 'int'>
>>> 'hi'.__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> {}.__class__
<class 'dict'>
>>> ().__class__
<class 'tuple'>
>>> (x for x in range(3)).__class__
<class 'generator'>
>>> def f(): pass
>>> f.__class__
<class 'function'>
Define a subclass by listing its base:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
Note: Base must be accessible in scope (e.g., imported, or defined in the same module).
class DerivedClassName(modname.BaseClassName):
>>> class Animal:
... def sound(self):
... return "generic sound"
...
>>> class Dog(Animal):
... def bark(self):
... return "woof"
...
>>> d = Dog()
>>> d.bark() # from Dog
'woof'
>>> d.sound() # inherited from Animal
'generic sound'
>>> class Animal:
... def speak(self):
... return "generic sound"
...
>>> class Dog(Animal):
... def speak(self):
... return "woof"
...
>>> d = Dog()
>>> d.speak() # override
'woof'
>>> Animal.speak(d) # call base directly
'generic sound'
>>> isinstance(5, int)
True
>>> isinstance(True, int) # bool is a subclass of int
True
>>> issubclass(bool, int)
True
>>> issubclass(float, int)
False
Subclass from multiple bases:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
Use case: combine behaviors from several parents.
>>> class Flyer:
... def fly(self):
... return "I can fly"
...
>>> class Swimmer:
... def swim(self):
... return "I can swim"
...
>>> class Duck(Flyer, Swimmer):
... pass
...
>>> d = Duck()
>>> d.fly()
'I can fly'
>>> d.swim()
'I can swim'
>>> class A: pass
>>> class B(A): pass
>>> class C(B): pass
>>> C.mro()
[<class '__main__.C'>, <class '__main__.B'>,
<class '__main__.A'>, <class 'object'>]
>>> class A:
... def greet(self): return "Hello from A"
...
>>> class B(A): pass
>>> class C(A): pass
>>> class D(B, C): pass
>>> D.mro()
[<class '__main__.D'>, <class '__main__.B'>,
<class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
>>> class A:
... def greet(self):
... return "Hello from A"
...
>>> class B(A):
... def greet(self):
... return super().greet() + " & B"
...
>>> class C(A):
... def greet(self):
... return super().greet() + " & C"
...
>>> class D(B, C):
... def greet(self):
... return super().greet() + " & D"
...
>>> d = D()
>>> d.greet()
'Hello from A & C & B & D'
>>> class A:
... def __init__(self):
... self.__hidden = 42
...
>>> a = A()
>>> a.__hidden # direct access fails
Traceback (most recent call last):
File "<python-input-44>", line 1, in <module>
a.__hidden
AttributeError: 'A' object has no attribute '__hidden'. Did you mean: '_A__hidden'?
>>> a._A__hidden # can still access
42
>>> class Base:
... def __init__(self):
... self.__secret = "base"
...
>>> class Child(Base):
... def __init__(self):
... super().__init__()
... self.__secret = "child"
...
>>> c = Child()
>>> c._Base__secret # from Base
'base'
>>> c._Child__secret # from Child
'child'
Note: mangling keeps names separate.
from dataclasses import dataclass
@dataclass
class Employee:
name: str
dept: str
salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000
Most container objects can be looped over using a for statement.
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
>>> s = 'abc'
>>> it = iter(s)
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
StopIteration
class Reverse:
"""Iterate over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index -= 1
return self.data[self.index]
>>> for char in Reverse("spam"):
... print(char)
...
m
a
p
s
def reverse(data):
for i in range(len(data)-1, -1, -1):
yield data[i]
>>> for char in reverse("golf"):
... print(char)
f
l
o
g
>>> sum(i*i for i in range(10)) # sum of squares
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x, y in zip(xvec, yvec)) # dot product
260
>>> list(data[i] for i in range(len("golf")-1, -1, -1))
['f', 'l', 'o', 'g']