Lecture 1 — CS 12 (Computer Programming II)
Second Semester, AY 2024-2025
Department of Computer Science
University of the Philippines Diliman
Administrivia
Statically typed Python
Reading assignment: Review of Pythonic features
Administrivia
Statically typed Python
Reading assignment: Review of Pythonic features
Problem: Find the bug in the code below:
from collections import Counter
def mode(xs):
if not xs:
return None
ctr = Counter(xs)
largest_count = max(ctr.values())
mode_candidate_count = list(ctr.values()).count(largest_count)
return ctr.most_common()[0][0] if mode_candidate_count == 1 else None
answer = mode([1, 2, 3, 1])
square_of_mode = answer**2
Is there an easier way to identify this?
Type hint/annotation: Explicit declaration of types of variables and return values
students = 93
nums = []
names = []
<variable-name>: <type> = <value>
students: int = 93
nums: list[int] = []
names: list[str] = []
def is_prime(n):
...
def print_larger_len(a, b):
...
def <function-name>(<arg>: <type>[, <arg>: <type>]) -> <return-type>:
<block>
def is_prime(n: int) -> bool:
...
def print_larger_len(a: str, b: str) -> None:
...
def update(spaceship):
...
Need to go through function body to find out
def update(spaceship: dict[str, int]) -> None: # Note: dict[str, int]
... # means the keys are strs
# while the values are ints
Above can be answered without going through function body; saves time and effort
Type checking: Process of checking whether type rules of PL are violated by code
Kind | When | Python |
---|---|---|
Static type checking | Before execution | Not done by default | Dynamic type checking | During execution | Done by default |
def f(a, b): # Intention is for both params to be ints
return a + b
f(1, 2) # Passes type checking
f(12, "hello") # Does not pass type checking;
# raises TypeError during execution (crash!)
In general, we do not want our programs to crash during runtime
def f(a: int, b: int) -> int:
return a + b
f(1, 2) # Should pass type checking
f(12, "hello") # Should not pass type checking
def f(a: int, b: int) -> int:
return a + b
f(1, 2)
# Argument of type "Literal['hello']" cannot be assigned to
# parameter "b" of type "int" in function "f";
# -> "Literal['hello']" is incompatible with "int"
f(12, "hello")
Please practice reading through type error messages
def f(a: int, b: int): # Return type is optional
return a + b
# Flagged by Pyright; must be `nums: list[int] = []`
nums = []
for num in range(1, 11):
nums.append(num)
Ensure your code passes Pyright strict mode before submission!
def f(n: int):
if n > 0:
return "POSITIVE"
elif n < 0:
return "NEGATIVE"
Note: Python functions that terminate without encountering a return statement automatically return None
def f(n: int):
if n > 0:
return "POSITIVE"
elif n < 0:
return "NEGATIVE" # None will be returned when n == 0
# f(n: int) -> (Literal['POSITIVE', 'NEGATIVE'] | None)
def f(n: int):
if n > 0:
return "POSITIVE"
elif n < 0:
return "NEGATIVE"
elif n == 0:
return "ZERO"
def f(n: int): # Pyright infers the return type
if n > 0: # of f correctly due to `else`
return "POSITIVE"
elif n < 0:
return "NEGATIVE"
else:
return "ZERO"
def f(nums: list[int] | None) -> int | None:
if nums is None: # How does Pyright behave when
return None # condition is removed?
nums.append(100) # What more specific type does
# Pyright infer here?
return sum(nums)
Very useful in ensuring types are as expected
Type | Dynamic typing | Static typing (homogeneity) | Static type annotation |
---|---|---|---|
list | Elements can be of any type | Elements must have same type | list[int], list[str], list[list[int]] |
tuple | – | – | tuple[int,int,int], tuple[str,bool] |
set | Elements can be of any type | Elements must have same type | set[int], set[str] | dict | Keys and values can be of any type | Keys must have same type; values must have same type | dict[str,int], dict[int,dict[int,str]] |
Problem: Find the bug in the code below:
from collections import Counter
def mode(xs: list[int]):
if not xs:
return None
ctr = Counter(xs)
largest_count = max(ctr.values())
mode_candidate_count = list(ctr.values()).count(largest_count)
return ctr.most_common()[0][0] if mode_candidate_count == 1 else None
answer = mode([1, 2, 3, 1])
square_of_mode = answer**2
Pyright can help with this
Type alias: Alternative name for existing type
Matrix = list[list[int]]
def mul_matrices(a: Matrix, b: Matrix) -> Matrix:
nrow = len(a)
ncol = len(b[0])
ret = [[0 for _ in range(ncol)] for _ in range(nrow)]
for i in range(nrow):
for j in range(ncol):
for k in range(nrow):
ret[i][j] += a[i][k] * b[k][j]
return ret
Literal type: Type having specific constant values
from typing import Literal, get_args # Must import from typing module
MyLiteral = Literal['a', 'b']
def f(x: MyLiteral): # Pyright can infer that that return type
if x == 'a': # of f is Literal[1, 2]
return 1
else:
return 2
print(get_args(MyLiteral)) # ('a', 'b')
f('c') # Invalid; Literal['c'] is not in Literal['a', 'b']
if f('a') == 0: # Invalid; Literal[0] is not in Literal[1, 2]
...
EnggProgram = Literal['ChE', 'CE', 'CoE', 'CS', 'EcE', 'EE', 'GE',
'IE', 'MatE', 'ME', 'MetE', 'EM']
EnggUnit = Literal['DChE', 'DCS', 'DGE', 'DIEOR', 'EEE', 'ICE', 'DME', 'DMMME']
def get_offering_unit(program: EnggProgram) -> EnggUnit:
if program == 'ChE':
return 'DChE'
elif program == 'CE':
return 'ICE'
elif program == 'CoE' or program == 'EcE' or program == 'EE':
return 'EEE'
elif program == 'GE':
return 'DGE'
elif program == 'MatE' or program == 'MetE' or program == 'EM':
return 'DMMME'
elif program == 'ME':
return 'DME'
# Recall that functions return None if allowed to reach the end
Pyright can tell you exactly which cases you missed!
EnggProgram = Literal['ChE', 'CE', 'CoE', 'CS', 'EcE', 'EE', 'GE',
'IE', 'MatE', 'ME', 'MetE', 'EM']
EnggUnit = Literal['DChE', 'DCS', 'DGE', 'DIEOR', 'EEE', 'ICE', 'DME', 'DMMME']
def get_offering_unit(program: EnggProgram) -> EnggUnit:
match program:
case 'ChE':
return 'DChE'
case 'CE':
return 'ICE'
case 'CoE' | 'EcE' | 'EE': # Value must be one of them;
return 'EEE' # more concise than if version
case 'CS':
return 'DCS'
case 'GE':
return 'DGE'
case 'IE':
return 'DIEOR'
case 'MatE' | 'MetE' | 'EM':
return 'DMMME'
case 'ME':
return 'DME'
Administrivia
Statically typed Python
Reading assignment: Review of Pythonic features
if len(elems) > 0:
...
if elems: # Easier to read
... # and write
Python collections can be used as boolean values
elems = [ 'a', 'b', 'c', 'd' ]
# 0 1 2 3
# -4 -3 -2 -1
# elems[-1] == 'd'
# elems[-len(elems)] == 'a'
elems = [ 'a', 'b', 'c', 'd' ] # len(elems) == 4
# 0 1 2 3 # i
# 4-4 4-3 4-2 4-1 # len(elems)-i
if word and word[-1] == 's': # Nonempty string is truthy
suffix = "'"
else:
suffix = "'s"
possessive_form = word + suffix # "student" -> "student's"
# "Cyrus" -> "Cyrus'"
possessive_form = f"{word}{suffix}" # Explicitly a string
if word and word[-1] == 's':
suffix = "'"
else:
suffix = "'s"
suffix = "'" if word and word[-1] == 's' else "'s"
# Indexes [start,stop) of sequence in increments of step
# similar to range
<sequence>[<start>:<stop>:<step>]
lst = [10, 11, 12, 13, 14]
print(lst[1:3]) # [11, 12]
tup = (10, 11, 12, 13, 14)
print(tup[1:3]) # (11, 12)
s = "abcde"
print(s[1:3]) # "bc"
lst = [10, 11, 12, 13, 14]
print(lst[1:4]) # [11, 12, 13]
print(lst[1:]) # [11, 12, 13, 14]
print(lst[:4]) # [10, 11, 12, 13]
print(lst[:]) # [10, 11, 12, 13, 14] (effectively copies list)
print(lst[::-1]) # [14, 13, 12, 11, 10] (list(reversed(lst)))
coords = (10, 20)
x = coords[0]
y = coords[1]
coords = (10, 20)
x, y = coords
temp = x
x = y
y = temp
x, y = y, x
a = [20, 30]
b = [50, 60]
c = [10] + a + [40] + b # [10, 20, 30, 40, 50, 60]
a = [20, 30]
b = [50, 60]
c = [10, *a, 40, *b] # [10, 20, 30, 40, 50, 60]
x = {'a': 1, 'b': 2}
y = {'c': 3}
z = {**x, **y, 'd': 4} # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
elems = ['Poring', 'Fabre', 'Lunatic', 'Chonchon']
for i in range(len(elems)): # [0, 1, 2, 3]
print(x[i])
for elem in elems:
print(elem)
Use range(len(elems)) only when index is necessary
elems = ['Tressa', 'Cyrus', 'Olberic', 'Primrose']
for i in range(len(elems)): # "#1: Tressa";
print(f'#{i+1}: {elems[i]}') # must index
for i, elem in enumerate(elems): # i starts at 0;
print(f'#{i+1}: {elem}') # no need to index
for n, elem in enumerate(elems, start=1): # n starts at 1; no need
print(f'#{n}: {elem}') # to index and increment
d = {'a': 1, 'b': 2, 'c': 3}
for key in d: # No need to do d.keys()
print(f'Key: {key} has value {d[key]}')
d = {'a': 1, 'b': 2, 'c': 3}
for key, value in d.items():
print(f'Key: {key} has value {value}')
elems = ['Poring', 'Fabre', 'Lunatic', 'Chonchon']
for i in range(len(elems) - 1, -1, -1): # i: [3, 2, 1, 0]
print(elems[i])
for i in range(len(elems)): # i: [0, 1, 2, 3]
print(elems[len(elems)-i]) # 3 - i: [3, 2, 1, 0]
Prone to off-by-one errors; can we do better?
for elem in reversed(elems):
print(elem)
for elem in elems[::-1]:
print(elem)
nums = []
for n in range(1, 11):
nums.append(n**2)
# [<expr> for <variable> in <iterable>] builds a new list
nums = [n**2 for n in range(1, 11)]
matrix = [[((r * 3) + c) for c in range(3)] for r in range(5)]
primes = []
for n in range(2, 101):
if is_prime(n):
primes.append(x)
# [<expr> for <variable> in <iterable> if <condition>];
# <expr> is only appended if <condition> is True
primes = [n for n in range(2, 101) if is_prime(n)]
strs = ['a', 'aa', 'b', 'abc', 'c', 'bbb']
lengths = set()
for s in strs:
lengths.add(len(s))
lengths = {len(s) for s in strs} # {1, 2, 3}
strs = ['a', 'aa', 'b', 'abc', 'c', 'bbb']
lengths = {}
for s in strs: # lengths['aa'] == 2
lengths[s] = len(s) # lengths['abc'] == 3
lengths = {s: len(s) for s in strs}