← Computer Programming I

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 between a and b (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

  1. Serialization (Dumping): Converting a Python object into a JSON string.
  2. 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$ JSON true
  • Python None $\rightarrow$ JSON null
  • 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")