Lecture#
Packages and Modules#
Organizing Python code into reusable units.
Create a file called packages_demo.py to follow along with the examples below.
What Are They?
Modular programming breaks a large task into smaller, manageable subtasks called modules.
A single
.pyfile.Contains functions, classes, and variables.
Example:
math_utils.py
A folder containing
.pyfiles.Must include
__init__.py(can be empty).Example:
shape/
Note
Since Python 3.3, __init__.py is technically optional (namespace packages), but it is required for regular packages and should always be included.
Note
Python has a large collection of standard modules. Standard and user-defined modules are imported the same way.
Making Packages Discoverable — Adding to sys.path
Python can only import packages that are on its module search path (sys.path). If your script and package live in sibling directories (e.g., lecture2/ and shape/), Python may not find the package by default.
import sys
import os
# Add the parent directory to sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
__file__— path to the current script.os.path.abspath()— resolves to a full absolute path.os.path.dirname()— goes up one directory level.sys.path.insert(0, ...)— adds the path to the front of the search list.
Tip
Place this at the very top of your script, before any other imports that depend on the path.
Import Strategies
There are four common ways to import names from a module.
Approach 1 — Full module path:
import shape.square
result = shape.square.compute_area(4)
Approach 2 — Alias:
import shape.square as sq
result = sq.compute_area(4)
Approach 3 — Import specific names (recommended):
from shape.square import compute_area, compute_perimeter
result = compute_area(4)
Approach 4 — Wildcard (avoid):
from shape.square import * # Namespace pollution risk!
Why Avoid Wildcard Imports?
import * dumps every name from a module into your current namespace, which can silently overwrite existing variables or functions.
from shape.square import * # brings in compute_area, compute_perimeter
from shape.circle import * # also brings in compute_area, compute_perimeter
result = compute_area(4) # Which version is this? circle!
compute_areafromsquareis silently overwritten bycircle’s version.No error, no warning — your code just computes the wrong thing.
Readers cannot tell which module a function came from.
Tip
Best practice: Use explicit named imports so it is always clear where each name originated.
from shape.square import compute_area as square_area
from shape.circle import compute_area as circle_area
Importing Packages from Anywhere
So far we have seen how to import sibling packages using sys.path.insert(). But what if the package is located somewhere else on the system?
Python provides several methods to make packages discoverable. In all cases, the path you add should be the parent directory of the package, not the package directory itself.
Package location |
Path to add |
|---|---|
|
|
|
|
Method 1 — PYTHONPATH Environment Variable
Set the PYTHONPATH environment variable in your shell before running the script. Python adds every directory in PYTHONPATH to sys.path automatically at startup.
# Add one directory (append to existing PYTHONPATH)
export PYTHONPATH="/opt/libs:$PYTHONPATH"
python3 my_script.py
# Add multiple directories
export PYTHONPATH="/opt/libs:/home/alice/projects/common:$PYTHONPATH"
python3 my_script.py
Now your script can import directly with no code changes:
import my_utils # Found in /opt/libs/
import shared_tools # Found in /home/alice/projects/common/
Warning
PYTHONPATH is session-specific — it resets when you close the terminal. Add it to your ~/.bashrc to make it permanent.
Method 2 — .pth Files
Drop a .pth file into Python’s site-packages directory. Each line is a path that gets added to sys.path automatically at startup.
First, find your site-packages directory:
python3 -c "import site; print(site.getsitepackages())"
Then create a .pth file in that directory:
# /usr/lib/python3.12/site-packages/enpm605.pth
/opt/libs
/home/alice/projects/common
Now every Python script on the system can import from those paths:
import my_utils # Found in /opt/libs/
import shared_tools # Found in /home/alice/projects/common/
Tip
This is a system-wide change. Use this for packages you want available to all your projects.
Method 3 — Editable Install (pip3 install)
The most robust approach. Add a pyproject.toml to your package and install it in editable mode.
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "shape2"
version = "0.1.0"
description = "Simple shape geometry utilities for ENPM605"
requires-python = ">=3.10"
Then install it:
cd <path to shape>
pip3 install -e . --break-system-packages
Works from anywhere — no path manipulation needed.
The
-eflag means changes take effect immediately without reinstalling.This is how real Python projects manage dependencies.
Tip
Recommended: This is the most portable and professional approach.
Summary of Discovery Approaches
Method |
Scope |
Best for |
|---|---|---|
|
Single script |
Quick fixes, sibling packages |
|
Terminal session |
Development and testing |
|
System-wide |
Shared libraries across projects |
|
System-wide |
Reusable packages (recommended) |
Note
For this course, we will primarily use sys.path.insert() and pip3 install
The __name__ Guard
When a module is run directly, its __name__ is set to "__main__". When imported, __name__ is set to the module’s name.
from shape.triangle import compute_area
print(compute_area(3, 2))
Note
This pattern allows a module to serve both as an importable library and as a standalone script.
Indentation#
Unlike C++ or Java which use braces {}, Python uses indentation to define blocks of code.
Create a file called indentation_demo.py to follow along.
Python’s Block Structure
def greeting(name):
print("Hello", name)
if name == "Alice":
print("Welcome back!")
void greeting(std::string name) {
std::cout << "Hello " << name << '\n';
if (name == "Alice") {
std::cout << "Welcome back!\n";
}
}
Warning
Mixing tabs and spaces causes IndentationError. Configure your editor to use 4 spaces per indent level (PEP 8 standard).
Boolean Type#
Truth values, truthiness, and the bool() function.
Create a file called boolean_demo.py to follow along with the examples below.
The bool Type
Python provides the Boolean type bool with exactly two values: True and False.
boolis a subclass ofint:Trueis1andFalseis0.In a condition, any non-zero value or non-empty sequence evaluates to
True.The built-in
bool()function converts a value to a Boolean.
print(bool(0)) # False
print(bool(0.0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool({})) # False
print(bool(None)) # False
print(bool(1)) # True
print(bool(-2)) # True
print(bool("hi")) # True
print(bool([1, 2])) # True
print(bool(" ")) # True (space!)
print(bool(0.001)) # True
Tip
Pythonic idiom: Use truthiness directly in conditions — write if my_list: instead of if len(my_list) > 0:.
Operators#
Arithmetic, relational, logical, membership, and identity operators.
Create a file called operators_demo.py to follow along with the examples below.
Arithmetic Operators
Operator |
Operation |
Example |
Result |
|---|---|---|---|
|
Addition |
|
|
|
Subtraction |
|
|
|
Multiplication |
|
|
|
Division (float) |
|
|
|
Floor division |
|
|
|
Modulus (remainder) |
|
|
|
Exponentiation |
|
|
# Floor division always rounds toward negative infinity
print(10 // 3) # 3
print(10 // -3) # -4 (not -3!)
# Augmented assignment operators
x = 10
x += 5 # x = x + 5 -> 15
x *= 2 # x = x * 2 -> 30
Relational Operators
Relational operators compare values and return True or False.
Let a = 5 and b = 3:
Operator |
Description |
Example |
|---|---|---|
|
Equal |
|
|
Not equal |
|
|
Greater than |
|
|
Less than |
|
|
Greater than or equal |
|
|
Less than or equal |
|
# Python supports chained comparisons
x = 5
print(1 < x < 10) # True (equivalent to 1 < x and x < 10)
print(1 < x > 3) # True
Logical Operators
Logical operators combine Boolean expressions.
Let a = True and b = False:
Operator |
Description |
Example |
|---|---|---|
|
|
|
|
|
|
|
Reverses the logical state |
|
# Short-circuit evaluation
x = 5
print(x > 0 and x < 10) # True
print(x > 10 or x == 5) # True
print(not (x == 5)) # False
Logical Operators with Non-Boolean Values
Python’s and and or don’t always return True or False — they return one of the actual operands.
and— Returns the first falsy value. If all truthy, returns the last value.or— Returns the first truthy value. If all falsy, returns the last value.not— Always returns abool.
# and: returns first falsy, or last value if all truthy
print("hello" and 0) # 0 ("hello" is truthy, so check 0 -> falsy)
print("hello" and "world") # "world" (both truthy, return last)
# or: returns first truthy, or last value if all falsy
print("hello" or 0) # "hello" (truthy, stop immediately)
print(0 or "default") # "default" (0 is falsy, check next)
# not: always returns a bool
print(not "") # True (empty string is falsy)
print(not "hello") # False (non-empty string is truthy)
Tip
Common pattern: Use or to provide default values. Example: name = user_input or "Anonymous" assigns "Anonymous" when user_input is empty or falsy.
Membership and Identity Operators
Test if an element belongs in a sequence.
Operator |
Description |
|---|---|
|
|
|
|
x = "hello"
print("h" in x) # True
print("he" in x) # True
print("O" in x) # False
print("z" not in x) # True
Compare memory locations of objects.
Operator |
Description |
|---|---|
|
Same object (same |
|
Different objects |
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True (same values)
print(a is b) # False (different objects)
print(a is c) # True (same object)
Important
Rule: Use == for value comparison. Use is only for None checks.
Exercise 1: Operators (5 min)
Predict the output of each expression before running the code.
# Arithmetic
print(17 // 5)
print(17 % 5)
print(2 ** 0.5)
print(-7 // 2)
# Logical with non-boolean values
print(0 or "default")
print("hello" and "world")
print(not [])
# Chained comparison
x = 15
print(10 < x < 20)
print(10 < x > 20)
Numeric Types#
Integers, floats, precision pitfalls, and interning.
Create a file called numeric_types_demo.py to follow along with the examples below.
Integers and Floats
Name |
Type |
Description |
Examples |
|---|---|---|---|
Integer |
|
Whole numbers (unlimited precision) |
|
Float |
|
Decimal numbers (64-bit IEEE 754) |
|
Complex |
|
Complex numbers |
|
# Python ints have unlimited precision
big = 10 ** 100
print(type(big)) # <class 'int'>
# Convert to int
print(int(3.7)) # 3 (truncates)
print(int("42")) # 42
print(int("101011", 2)) # 43 (binary)
# Float precision limits
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False!
# Convert to float
print(float("3.5")) # 3.5
print(float(3)) # 3.0
print(float("inf")) # inf
Warning
Never compare floats with ==. Use math.isclose(a, b) or check abs(a - b) < epsilon instead.
Integer and String Interning
CPython caches (“interns”) small integers and compile-time string constants to save memory and speed up comparisons.
a, b = 20, 20
print(a is b) # True (cached)
a, b = -5, -5
print(a is b) # True (cached)
# Large ints in the same statement
a, b = 200000000000, 200000000000
print(a is b) # True (compile-time)
a = "hello"
b = "hello"
c = "h" + "ello" # Compile-time
d = "".join(["h","e","l","l","o"])
print(a is b) # True
print(a is c) # True (folded at compile)
print(a is d) # False (runtime-built)
import sys
e = sys.intern(d)
print(a is e) # True (manually interned)
Warning
Never rely on interning for correctness. Always use == for value comparison. Use is only for None checks.
String Type#
Strings, escape sequences, formatting, methods, indexing, and slicing.
Create a file called strings_demo.py to follow along with the examples below.
String Basics
A Python string (str) is an immutable sequence of characters.
# Single and double quotes are equivalent
greeting = "Hello, World!"
greeting2 = 'Hello, World!'
# Triple quotes for multi-line strings
description = """This is a
multi-line string."""
# String conversion
number = 123
number_str = str(number)
print(type(number_str)) # <class 'str'>
Escape Sequences:
print("Line 1\nLine 2") # Newline
print("Col1\tCol2\tCol3") # Tab
print("She said: \"Hi!\"") # Escaped quotes
print('It\'s Python!') # Escaped apostrophe
print(r"C:\Users\tony\notes") # Raw string (no escapes)
String Interpolation
There are three ways to format strings in Python.
Old-style (``%`` operator) — Legacy, avoid in new code:
name, age = "Alice", 25
print("Name: %s, Age: %d" % (name, age))
str.format() — More flexible:
print("Name: {}, Age: {}".format(name, age))
print("Name: {name}, Age: {age}".format(name="Alice", age=25))
f-strings (Python 3.6+) — Recommended:
print(f"Name: {name}, Age: {age}")
print(f"Next year: {age + 1}")
print(F"Pi: {3.14159:.2f}") # Format specifier: 3.14. Note: uppercase F works as well
print(f"{'hello':>20}") # Right-align in 20 chars
Tip
Use f-strings for all new code. They are faster, more readable, and support inline expressions.
String Concatenation and Methods
# + operator
first = "John"
last = "Doe"
full = first + " " + last
# join() method (efficient)
words = ["Hello", "World"]
sentence = " ".join(words)
print(sentence) # Hello World
# Repetition
print("=" * 40)
s = "Hello, World!"
print(s.upper()) # HELLO, WORLD!
print(s.lower()) # hello, world!
print(s.capitalize()) # Hello, world!
print(s.swapcase()) # hELLO, wORLD!
print(s.strip()) # Remove whitespace
print(s.replace("World", "Python"))
print(s.split(", ")) # ['Hello', 'World!']
print(s.find("World")) # 7
print(s.count("l")) # 3
print(s.startswith("Hello")) # True
Note
String methods return new strings — they never modify the original (strings are immutable).
Indexing
Strings are ordered sequences, so each character has a positional index.
String |
|
|
|
|
|
+ Index |
0 |
1 |
2 |
3 |
4 |
− Index |
−5 |
−4 |
−3 |
−2 |
−1 |
greeting = "hello"
# Positive indexing
print(greeting[0]) # 'h'
print(greeting[4]) # 'o'
# Negative indexing
print(greeting[-1]) # 'o'
print(greeting[-5]) # 'h'
# Common errors
# print(greeting[5]) # What is the output?
# greeting[0] = 'H' # What is the output?
Slicing
Slicing extracts a substring by specifying a range of indices using the syntax [start:stop:stride]:
start: Starting index (inclusive), defaults to 0.stop: Ending index (exclusive), defaults to end of string.stride: Step size, defaults to 1.
greeting = "hello" # 'h':0:-5, 'e':+1:-4, 'l':+2:-3, 'l':+3:-2, 'o':+4:-1
# Basic slicing
print(greeting[0:3]) # "hel"
print(greeting[:3]) # "hel" (start defaults to 0)
print(greeting[2:]) # "llo" (stop defaults to end)
print(greeting[:]) # "hello" (entire string)
# Negative indices
print(greeting[-5:-2]) # "hel"
print(greeting[-3:]) # "llo"
# With stride
print(greeting[::2]) # "hlo" (every 2nd character)
print(greeting[::-1]) # "olleh" (reverse!)
print(greeting[4:1:-1])# "oll"
Exercise 2: Strings (10 min)
Part A: Predict the outputs before running.
text = "Learn Python, be happy!"
print(text[6:12])
print(text[-6:])
print(text[::3])
Part B: Using the variable quote = "Learn Python, be happy!"
Task 1: Extract
"Python"using only positive indices.Task 2: Extract
"Python"using only negative indices.Task 3: Reverse
"Python"to get"nohtyP"using slicing.Task 4: Reverse the entire string.
Part C: Print only the second half of a string.
text = "HelloWorld"
# second_half = ??
# print(second_half) # Expected: World
Control Flow#
Making decisions with if, elif, and else.
Create a file called control_flow_demo.py to follow along with the examples below.
The if Statement
Selection determines which code block executes based on conditions.
x = 10
if x > 0:
print("x is positive")
print("always runs")
x = -3
if x >= 0:
print("Non-negative")
else:
print("Negative")
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"Grade: {grade}") # Grade: B
Conditional Expressions
Python supports single-line conditional assignment (the ternary expression).
age = 20
status = "adult" if age >= 18 else "minor"
print(status) # "adult"
# Equivalent to:
if age >= 18:
status = "adult"
else:
status = "minor"
Nested Conditions
temperature = 25
humidity = 80
if temperature > 30:
if humidity > 70:
print("Hot and humid")
else:
print("Hot and dry")
elif temperature > 20:
print("Pleasant") # This runs
else:
print("Cool")
Exercise 3: Control Flow (10 min)
Write a program that determines if a year is a leap year.
year = 2024
# A year is a leap year if:
# - Divisible by 4 AND not divisible by 100
# - OR divisible by 400
# Print "Leap year" or "Not a leap year"
Tip
Use % (modulus) to check divisibility. year % 4 == 0 means divisible by 4.
Putting It All Together#
Exercise 4: Robot Status Monitor (15 min)
Write a program that monitors a robot’s status using concepts from today’s lecture.
# Robot parameters
robot_name = "Waffle_01"
battery = 65
speed = 0.8
status_log = "IDLE:MOVING:CHARGING:MOVING:IDLE"
# 1. Use an f-string to print: "Robot Waffle_01 | Battery: 65%"
# 2. Classify battery level using if/elif/else:
# >= 80: "OK", 50-79: "LOW", 20-49: "WARNING", < 20: "CRITICAL"
# 3. Use string methods to:
# a) Count how many times "MOVING" appears in status_log
# b) Split status_log by ":" into a list
# c) Check if the last status is "IDLE"
# 4. Use slicing to extract the first status entry from status_log
# 5. Create a formatted status message:
# "Waffle_01 | Battery: LOW | Speed: 0.80 m/s | States: 5"
Summary#
Packages & Modules — Organize code; use
from ... import ...(Approach 3)Indentation — Defines code blocks; use 4 spaces
Operators — Arithmetic, relational, logical, membership, identity
Boolean Type — Truthiness, falsy values,
bool()
Numeric Types —
int(unlimited),float(IEEE 754), interningStrings — Immutable sequences; f-strings, methods, indexing, slicing
Control Flow —
if/elif/else, ternary expressions
Note
Reminder: Review and experiment with all provided code before next class.
Preview: What’s Next in L3#
Lists and list methods
Tuples and unpacking
Dictionaries
Sets
Loops (
for,while)List comprehensions
Note
Today’s lecture gives you the foundational tools — operators, strings, and control flow — that you will use constantly from L3 onward.