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.

📄 Module
  • A single .py file.

  • Contains functions, classes, and variables.

  • Example: math_utils.py

📁 Package
  • A folder containing .py files.

  • 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_area from square is silently overwritten by circle’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

/opt/libs/my_utils/

/opt/libs

/home/alice/projects/common/shared_tools/

/home/alice/projects/common

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.

Listing 1 shape/pyproject.toml#
[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 -e flag 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

sys.path.insert()

Single script

Quick fixes, sibling packages

PYTHONPATH

Terminal session

Development and testing

.pth files

System-wide

Shared libraries across projects

pip3 install

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.

  • bool is a subclass of int: True is 1 and False is 0.

  • 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.

❌ Falsy Values
print(bool(0))       # False
print(bool(0.0))     # False
print(bool(""))      # False
print(bool([]))      # False
print(bool({}))      # False
print(bool(None))    # False
✅ Truthy Values
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

7 + 3

10

-

Subtraction

7 - 3

4

*

Multiplication

7 * 3

21

/

Division (float)

7 / 3

2.333...

//

Floor division

7 // 3

2

%

Modulus (remainder)

7 % 3

1

**

Exponentiation

2 ** 10

1024

# 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

a == b is False

!=

Not equal

a != b is True

>

Greater than

a > b is True

<

Less than

a < b is False

>=

Greater than or equal

a >= 5 is True

<=

Less than or equal

a <= b is False

# 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

and

True if both operands are True

a and b is False

or

True if at least one is True

a or b is True

not

Reverses the logical state

not a is False

# 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 a bool.

# 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
🔍 Membership Operators

Test if an element belongs in a sequence.

Operator

Description

in

True if found

not in

True if not found

x = "hello"
print("h" in x)      # True
print("he" in x)     # True
print("O" in x)      # False
print("z" not in x)  # True
🆔 Identity Operators

Compare memory locations of objects.

Operator

Description

is

Same object (same id)

is not

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

int

Whole numbers (unlimited precision)

1, -42, 2000

Float

float

Decimal numbers (64-bit IEEE 754)

2.5, -0.001, 1e10

Complex

complex

Complex numbers

1+2j, 3+8j

🔢 Integer Type
# 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 Type
# 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.

🔢 Integer Interning
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)
🔤 String Interning
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
🔗 Concatenation
# + operator
first = "John"
last = "Doe"
full = first + " " + last

# join() method (efficient)
words = ["Hello", "World"]
sentence = " ".join(words)
print(sentence)  # Hello World

# Repetition
print("=" * 40)
🛠️ Common Methods
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

'h'

'e'

'l'

'l'

'o'

+ 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 Typesint (unlimited), float (IEEE 754), interning

  • Strings — Immutable sequences; f-strings, methods, indexing, slicing

  • Control Flowif/elif/else, ternary expressions

Note

Reminder: Review and experiment with all provided code before next class.

Preview: What’s Next in L3#

📖 L3: Python Fundamentals — Part II
  • 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.