Week 13 Lecture: Modules and Interacting with the Web (APIs)
1. The Standard Library & Importing Modules
The “Batteries Included” Philosophy
Python is often described as having a “batteries included” philosophy. This means that when you install Python, it comes equipped with a massive library of pre-written code called the Standard Library. This library allows you to perform complex tasks—like generating random numbers, handling dates, or performing advanced mathematics—without having to write the code from scratch.
A Module is simply a Python file (.py) containing functions, variables, and classes. To utilize this code in your own programs, you must import it. This practice improves efficiency (avoiding “reinventing the wheel”), reliability (standard libraries are rigorously tested), and organization.
Importing Syntax and Usage
1. Importing an Entire Module
The most common way to use a module is to import the whole file. You then access its contents using dot notation (module_name.item).
import math
# Calling a function
print(math.sqrt(16)) # Output: 4.0
If you inspect the math module using print(math.__file__), you might notice the file extension is .so (Shared Object) rather than .py. This indicates that the math module is actually compiled C code, which allows mathematical operations to run significantly faster than standard Python code.
Common math Constants and Functions:
- Constants: These do not require parentheses.
math.pi: Returns the value of $\pi$.math.e: Returns Euler’s number.math.inf: Represents infinity (larger than any integer you can type).
- Functions: These require parentheses and arguments.
math.log(10): Returns the natural logarithm (base $e$) of 10.math.log(100, 10): Returns the logarithm of 100 with base 10.
2. Importing Specific Items
If you only need a specific function, you can import it directly. This allows you to use the function without typing the module name every time.
from math import sqrt
# No module name needed
print(sqrt(25))
Note: While slightly more efficient in memory usage, the choice between this method and importing the whole module is usually a matter of code readability.
3. The random Module
This module is essential for generating non-deterministic data.
random.random(): Returns a float between 0.0 and 1.0.random.randint(a, b): Returns a random integer betweenaandb(inclusive).random.choice(sequence): Picks a random element from a sequence (like a string or a list).
from random import randint, choice
print(randint(1, 10)) # Random integer 1-10
print(choice(['a', 'b'])) # Randomly picks 'a' or 'b'
print(choice('hello')) # Randomly picks a letter from the string
4. Aliasing and the datetime Module
You can give a module a “nickname” (alias) using the as keyword to save typing.
import datetime as dt
# Accessing the datetime class inside the dt module
now = dt.datetime.now()
print(now)
# Output format: Year-Month-Day Hour:Minute:Second.Microsecond
Formatting Dates:
The datetime object contains a method called strftime (String Format Time), which allows you to present the date as a formatted string using special codes:
%d: Day%m: Month (number)%Y: Year (full) vs%y(short)%D: Complete date style
# Create a specific date
future = dt.date(2026, 1, 1) # Year, Month, Day
Example: Secure Password Generator
Problem: Create a function that generates a secure password of a specified length using the string and random modules. The password must contain letters, digits, and punctuation. The minimum length is 8.
Solution:
import string
import random
def generate_password(length):
# Enforce minimum length constraint
if length < 8:
length = 8
# Alternatively: raise ValueError('Length must be at least 8')
# string module provides pre-defined character sets
# ascii_letters: 'abcdef...ABCDEF...'
# digits: '0123456789'
# punctuation: '!"#$%&...'
pool = string.ascii_letters + string.digits + string.punctuation
password_chars = []
for _ in range(length):
# Efficiently pick a random character from the pool
char = random.choice(pool)
password_chars.append(char)
# Use .join() for efficient string construction
return "".join(password_chars)
print(generate_password(12))
2. Serialization with JSON
Understanding JSON
When programs communicate over a network, they cannot exchange complex Python objects (like lists or dictionaries) directly. They can only exchange text. JSON (JavaScript Object Notation) is the industry standard format for this text-based data exchange.
Despite the name “JavaScript,” JSON is language-independent.
- Object: A way to group information (like a Python dictionary).
- Notation: The specific writing style (curly braces
{}, colons:, double quotes"").
Serialization vs. Deserialization
- Serialization (Dumping): Converting a Python object into a JSON string.
- Deserialization (Loading): Converting a JSON string back into a Python object so code can interact with it.
Syntax Differences
When converting Python to JSON using json.dumps():
- Python
True$\rightarrow$ JSONtrue - Python
None$\rightarrow$ JSONnull - Python
'Single Quotes'$\rightarrow$ JSON"Double Quotes"(JSON strictly requires double quotes).
Code Example
import json
# 1. Serialization
person = {'name': 'Sukhrob', 'is_active': True, 'certificate': None}
json_string = json.dumps(person)
print(json_string)
# Output: {"name": "Sukhrob", "is_active": true, "certificate": null}
# 2. Deserialization
# We must convert the string back to a dict to access keys
python_dict = json.loads(json_string)
print(python_dict['name']) # Output: Sukhrob
Example: The Log Parser
Problem: You receive a raw log string containing a list of server events. Parse the string and count the errors.
Solution:
import json
raw_log = '[{"status": "OK", "msg": "Boot"}, {"status": "ERROR", "msg": "Disk full"}]'
try:
# Convert string -> List of Dictionaries
events = json.loads(raw_log)
error_count = 0
last_error_msg = ""
for event in events:
if event['status'] == "ERROR":
error_count += 1
last_error_msg = event['msg']
print(f"Errors: {error_count}, Last: {last_error_msg}")
except json.JSONDecodeError:
print("Error: The string provided was not valid JSON.")
Note: We use try/except to catch json.JSONDecodeError. If the input string is missing a bracket or a quote, the program will crash without this error handling.
3. HTTP Requests & APIs
What is an API?
An API (Application Programming Interface) is a mechanism that allows two different computer programs to communicate.
- Application: Computer program.
- Programming: The code.
- Interface: The meeting point/connection.
Think of a website like a university building. The “Main Entrance” is the visual website designed for humans (HTML/CSS). The “Side Door” is the API, designed for developers to request raw data without decorations.
Installing Requests
To interact with APIs, we use the requests library. It is not built-in, so we must install it using PIP (a recursive acronym for “Pip Installs Packages”).
Terminal command:
pip install requests
Making GET Requests
A GET request asks the server to retrieve data.
1. Basic Request:
import requests
# The URL often contains a Domain, Subdomain, and Path
response = requests.get('https://api.adviceslip.com/advice')
# status_code 200 means Success
if response.status_code == 200:
# .json() automatically converts the response text into a Python Dict
data = response.json()
print(data['slip']['advice'])
2. Path Parameters: Some APIs change their output based on the URL path.
# Fetching data for a specific resource (Pikachu)
response = requests.get('https://pokeapi.co/api/v2/pokemon/pikachu')
3. Query Parameters:
Query parameters allow you to filter data. They appear in URLs after a ? and are separated by &. Instead of constructing messy strings manually, pass a dictionary to the params argument.
# Desired URL: https://api.agify.io/?name=sukhrob&country_id=UZ
my_params = {
"name": "sukhrob",
"country_id": "UZ"
}
response = requests.get("https://api.agify.io/", params=my_params)
4. Headers and API Keys: Many APIs require an API Key for authentication (to track usage or charge for access). Keys are usually sent in the Headers (metadata) rather than the URL for security and cleanliness.
headers = {"x-api-key": "YOUR_SECRET_KEY"}
response = requests.get("https://api.thecatapi.com/v1/images/search", headers=headers)
Making POST Requests (Sending Data)
A POST request is used when you need to send data to a server to create something new or process a complex request (like an AI chat message).
Environment Variables: Never hardcode secrets (like API keys) directly in your code. Use environment variables.
import os
api_key = os.getenv('OPENROUTER_API_KEY')
Sending JSON Payloads:
When using POST, you often send a JSON payload via the data argument.
import json
import requests
import os
from pprint import pprint # Pretty Print for easier reading
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
"Content-Type": "application/json"
}
# The data we want to send
payload = {
"model": "x-ai/grok-4.1-fast",
"messages": [{"role": "user", "content": "Hello AI"}]
}
# Convert dict to JSON string
json_payload = json.dumps(payload)
# Send POST request
response = requests.post(url, headers=headers, data=json_payload)
if response.status_code == 200:
result = response.json()
pprint(result)
# Accessing nested data
print(result["choices"][0]["message"]["content"])
Example: Country Information Finder
Problem: Fetch the population and capital of a specific country (e.g., Uzbekistan) using the REST Countries API. Handle potential connection errors.
Solution:
import requests
def get_country_info(country_name):
url = f"https://restcountries.com/v3.1/name/{country_name}"
try:
response = requests.get(url)
if response.status_code == 200:
# The API returns a list of matches, we take the first one [0]
data = response.json()[0]
# Navigating nested JSON
common_name = data['name']['common']
capital = data['capital'][0]
population = data['population']
print(f"Country: {common_name}")
print(f"Capital: {capital}")
print(f"Population: {population:,}") # Format with commas
elif response.status_code == 404:
print("Error: Country not found.")
else:
print(f"Server Error: {response.status_code}")
except requests.exceptions.RequestException:
# Catches DNS failures, no internet, etc.
print("Error: Could not connect to the internet.")
get_country_info("uzbekistan")
This content will be available starting December 23, 2025.