Python Input and Output

Week 2: Python Fundamentals - Interacting with Users and Files

Session Overview

Welcome to our exploration of Python's input and output operations! Today, we'll learn how Python programs can interact with users through the console, read from and write to files, and format data for presentation. These fundamental skills will enable you to create interactive programs, process data from external sources, and present information in a meaningful way.

Console Input and Output

Basic Output with print()

The print() function is the most common way to display output in Python:

# Basic printing
print("Hello, World!")  # Displays: Hello, World!

# Printing multiple items
print("The answer is", 42)  # Displays: The answer is 42

# Using variables
name = "Alice"
age = 30
print(name, "is", age, "years old")  # Displays: Alice is 30 years old

The print() function has several useful parameters:

# Customizing separators between items
print("apple", "banana", "cherry", sep=", ")  # Displays: apple, banana, cherry

# Customizing end character (default is newline)
print("Hello", end=" ")
print("World!")  # Displays: Hello World!

# Redirecting output to a file
with open("output.txt", "w") as f:
    print("This goes to the file", file=f)

# Handling non-string objects
print([1, 2, 3])  # Displays: [1, 2, 3]
print({"name": "Alice", "age": 30})  # Displays: {'name': 'Alice', 'age': 30}

Basic Input with input()

The input() function allows users to enter data from the keyboard:

# Basic input
name = input("Enter your name: ")
print("Hello,", name)

# Note: input() always returns a string!
age_str = input("Enter your age: ")
age = int(age_str)  # Convert string to integer
print("Next year, you will be", age + 1)

# Combining input and conversion in one step
height = float(input("Enter your height in meters: "))
print("Your height in centimeters is", height * 100)

# Simple yes/no input
response = input("Do you like Python? (yes/no): ").lower()
if response == "yes":
    print("Great choice!")
else:
    print("You might change your mind!")

Analogy: Console I/O as a Conversation

Think of console input and output as a conversation between your program and the user:

  • print() is like your program speaking to the user
  • input() is like your program listening to the user
  • Just as in a conversation, the program needs to clearly communicate what input it expects
  • And like understanding speech in different languages, the program needs to convert the input to the right type

Good programs, like good conversationalists, provide clear prompts, listen carefully to responses, and give meaningful feedback.

Formatting Output

Python offers several ways to format output for better presentation:

String Concatenation

# Simple concatenation with + (only works with strings)
name = "Alice"
greeting = "Hello, " + name + "!"
print(greeting)  # Displays: Hello, Alice!

# Need to convert non-strings to strings first
age = 30
message = name + " is " + str(age) + " years old."
print(message)  # Displays: Alice is 30 years old.

F-strings (Python 3.6+)

F-strings provide a concise and readable way to embed expressions in string literals:

# Basic f-string
name = "Alice"
age = 30
print(f"{name} is {age} years old.")  # Displays: Alice is 30 years old.

# Expressions in f-strings
print(f"{name} will be {age + 10} years old in 10 years.")

# Formatting numbers
pi = 3.14159265359
print(f"Pi to 2 decimal places: {pi:.2f}")  # Displays: Pi to 2 decimal places: 3.14

# Width and alignment
for i in range(1, 6):
    print(f"{i:2d} {i*i:3d} {i*i*i:4d}")
# Displays:
#  1   1    1
#  2   4    8
#  3   9   27
#  4  16   64
#  5  25  125

# Named placeholders with dictionaries
person = {"name": "Alice", "age": 30}
print(f"{person['name']} is {person['age']} years old.")

str.format() Method

The format() method is another way to format strings:

# Basic formatting
name = "Alice"
age = 30
print("{} is {} years old.".format(name, age))

# Positional arguments
print("{0} is {1} years old. {0} lives in New York.".format(name, age))

# Named arguments
print("{name} is {age} years old.".format(name=name, age=age))

# Reusing the person dictionary
print("{name} is {age} years old.".format(**person))  # Unpacking dictionary

# Number formatting
print("Pi to 3 decimal places: {:.3f}".format(3.14159))  # Displays: Pi to 3 decimal places: 3.142

% Operator (Older Style)

The % operator is an older way to format strings, still found in legacy code:

# Basic formatting
name = "Alice"
age = 30
print("%s is %d years old." % (name, age))

# Named placeholders
print("%(name)s is %(age)d years old." % {"name": name, "age": age})

# Number formatting
print("Pi to 4 decimal places: %.4f" % 3.14159)  # Displays: Pi to 4 decimal places: 3.1416

Format Specifiers

Format specifiers allow precise control over how values are displayed:

# Common format specifiers (work with both f-strings and str.format())

# Width and alignment
for name in ["Alice", "Bob", "Charlie"]:
    print(f"{name:10}")  # Right-aligned in 10-character field
    
# Left, right, and center alignment
print(f"{'left':10}")     # Left-aligned (default for strings)
print(f"{'right':>10}")   # Right-aligned
print(f"{'center':^10}")  # Center-aligned

# Fill character
print(f"{'test':*^10}")   # '*****test*****'

# Number formatting
print(f"{123:05d}")       # '00123' (zero-padded)
print(f"{123:+d}")        # '+123' (show sign)
print(f"{-123:+d}")       # '-123'
print(f"{-123: d}")       # '-123' (space for positive numbers)
print(f"{123: d}")        # ' 123'

# Float formatting
print(f"{3.14159:.2f}")   # '3.14' (2 decimal places)
print(f"{3.14159:+.2f}")  # '+3.14' (with sign)
print(f"{3.14159:06.2f}") # '003.14' (zero-padded, 6 total width)

# Percentage
print(f"{0.25:.1%}")      # '25.0%' (as percentage)

# Scientific notation
print(f"{1000000:.2e}")   # '1.00e+06'

# Binary, octal, hex
print(f"{42:b}")          # '101010' (binary)
print(f"{42:o}")          # '52' (octal)
print(f"{42:x}")          # '2a' (hex, lowercase)
print(f"{42:X}")          # '2A' (hex, uppercase)
print(f"{42:#x}")         # '0x2a' (hex with prefix)

File Input and Output Basics

Python makes it easy to read from and write to files:

Opening and Closing Files

# Opening a file (creates it if it doesn't exist)
file = open("example.txt", "w")  # 'w' for write mode
file.write("Hello, File I/O!")
file.close()  # Always close files when done

# Better approach using 'with' statement (automatically closes file)
with open("example.txt", "w") as file:
    file.write("Hello, File I/O using 'with'!")
    # File automatically closed when the block exits

Common file modes include:

Writing to Files

# Write strings to a file
with open("fruits.txt", "w") as file:
    file.write("apple\n")  # \n for newline
    file.write("banana\n")
    file.write("cherry\n")

# Using writelines() for multiple lines
fruits = ["apple\n", "banana\n", "cherry\n"]
with open("fruits2.txt", "w") as file:
    file.writelines(fruits)

# Using print() with file parameter
with open("fruits3.txt", "w") as file:
    print("apple", file=file)
    print("banana", file=file)
    print("cherry", file=file)  # print() adds newlines automatically

Reading from Files

# Reading entire file at once
with open("fruits.txt", "r") as file:
    content = file.read()
    print(content)

# Reading line by line
with open("fruits.txt", "r") as file:
    line = file.readline()
    while line:
        print(line, end="")  # readline() keeps newline characters
        line = file.readline()

# Using readlines() to get a list of lines
with open("fruits.txt", "r") as file:
    lines = file.readlines()
    print(lines)  # ['apple\n', 'banana\n', 'cherry\n']

# Most efficient way: iterating over the file object
with open("fruits.txt", "r") as file:
    for line in file:
        print(line, end="")

File Positions

# Moving the file pointer
with open("fruits.txt", "r") as file:
    first_line = file.readline()
    print(first_line)
    
    # Get current position
    position = file.tell()
    print(f"Current position: {position}")
    
    # Move to beginning
    file.seek(0)
    
    # Read first 5 characters
    start = file.read(5)
    print(start)  # "apple"

Analogy: Files as Notebooks

Think of files as notebooks:

  • Opening a file is like taking a notebook off a shelf
  • Writing to a file is like writing in the notebook
  • Reading from a file is like reading what's in the notebook
  • The file pointer is like your finger keeping your place as you read
  • Closing the file is like putting the notebook back on the shelf
  • The 'with' statement is like having an assistant who always puts the notebook back for you

Just as you would close a notebook when done to keep it safe, properly closing files ensures data integrity and resource management.

Advanced File Operations

Working with Binary Files

# Writing binary data
with open("binary_file.bin", "wb") as file:
    file.write(b"Binary data: \x00\x01\x02\x03")

# Reading binary data
with open("binary_file.bin", "rb") as file:
    binary_data = file.read()
    print(binary_data)  # b'Binary data: \x00\x01\x02\x03'
    
    # Converting to bytes
    byte_array = bytearray(binary_data)
    print(byte_array)  # bytearray(b'Binary data: \x00\x01\x02\x03')

Working with CSV Files

CSV (Comma-Separated Values) is a common format for tabular data:

import csv

# Writing CSV data
with open("people.csv", "w", newline="") as file:
    writer = csv.writer(file)
    # Write header row
    writer.writerow(["Name", "Age", "City"])
    # Write data rows
    writer.writerow(["Alice", 30, "New York"])
    writer.writerow(["Bob", 25, "Los Angeles"])
    writer.writerow(["Charlie", 35, "Chicago"])

# Reading CSV data
with open("people.csv", "r", newline="") as file:
    reader = csv.reader(file)
    # Skip header
    header = next(reader)
    print(f"Header: {header}")
    # Process data rows
    for row in reader:
        print(f"{row[0]} is {row[1]} years old and lives in {row[2]}")

# Using dictionaries with CSV
with open("people_dict.csv", "w", newline="") as file:
    fieldnames = ["Name", "Age", "City"]
    writer = csv.DictWriter(file, fieldnames=fieldnames)
    
    writer.writeheader()
    writer.writerow({"Name": "Alice", "Age": 30, "City": "New York"})
    writer.writerow({"Name": "Bob", "Age": 25, "City": "Los Angeles"})

# Reading CSV as dictionaries
with open("people_dict.csv", "r", newline="") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(f"{row['Name']} is {row['Age']} years old and lives in {row['City']}")

Working with JSON Files

JSON (JavaScript Object Notation) is a popular format for structured data:

import json

# Creating Python data
data = {
    "people": [
        {"name": "Alice", "age": 30, "city": "New York", "active": True},
        {"name": "Bob", "age": 25, "city": "Los Angeles", "active": False},
        {"name": "Charlie", "age": 35, "city": "Chicago", "active": True}
    ],
    "organization": "Example Corp",
    "founded": 2010
}

# Writing JSON to a file
with open("data.json", "w") as file:
    json.dump(data, file, indent=4)  # indent for pretty formatting

# Reading JSON from a file
with open("data.json", "r") as file:
    loaded_data = json.load(file)
    print(loaded_data["organization"])
    for person in loaded_data["people"]:
        print(f"{person['name']} is {person['age']} years old")

# Converting to/from JSON strings
json_string = json.dumps(data, indent=2)
print(json_string)

parsed_data = json.loads(json_string)
print(parsed_data["founded"])

Error Handling in I/O Operations

I/O operations can fail for various reasons. It's important to handle these errors gracefully:

Handling File I/O Errors

# Basic try-except for file operations
try:
    with open("nonexistent_file.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("The file does not exist!")
except PermissionError:
    print("You don't have permission to access this file!")
except Exception as e:
    print(f"An error occurred: {e}")

# Handling multiple files
def safely_read_file(filename):
    try:
        with open(filename, "r") as file:
            return file.read()
    except FileNotFoundError:
        print(f"Warning: {filename} not found, skipping.")
        return None

# Process multiple files, continuing even if some fail
files = ["file1.txt", "nonexistent.txt", "file2.txt"]
contents = {}

for filename in files:
    content = safely_read_file(filename)
    if content:
        contents[filename] = content

Handling User Input Errors

# Robust user input with validation
def get_integer_input(prompt):
    """Get an integer input from the user, with validation."""
    while True:
        try:
            value = int(input(prompt))
            return value
        except ValueError:
            print("Invalid input. Please enter a valid integer.")

def get_float_input(prompt):
    """Get a float input from the user, with validation."""
    while True:
        try:
            value = float(input(prompt))
            return value
        except ValueError:
            print("Invalid input. Please enter a valid number.")

# Example usage
age = get_integer_input("Enter your age: ")
height = get_float_input("Enter your height in meters: ")

Context Managers for Resource Management

Context managers (using the with statement) help ensure resources are properly managed:

Using Built-in Context Managers

# File as context manager
with open("example.txt", "w") as file:
    file.write("Using context manager")
    # File automatically closed, even if an exception occurs

# Multiple context managers
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
    content = infile.read()
    outfile.write(content.upper())

Creating Custom Context Managers

class Timer:
    """A context manager for timing code execution."""
    
    def __enter__(self):
        import time
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.end_time = time.time()
        self.elapsed = self.end_time - self.start_time
        print(f"Elapsed time: {self.elapsed:.6f} seconds")
        # Return False to let exceptions propagate, True to suppress them
        return False

# Using our custom context manager
with Timer():
    # Code to time
    result = 0
    for i in range(1000000):
        result += i
    print(f"Sum: {result}")  # Sum will be displayed, then elapsed time

The contextlib Module

from contextlib import contextmanager

@contextmanager
def file_opener(filename, mode):
    """Simplified file opener context manager."""
    try:
        f = open(filename, mode)
        yield f
    finally:
        f.close()

# Using our decorator-based context manager
with file_opener("example.txt", "w") as file:
    file.write("Using contextlib.contextmanager")

# Creating a do-nothing context manager
@contextmanager
def no_operation():
    print("Entering context")
    yield
    print("Exiting context")

with no_operation():
    print("Inside the context block")

Practical Examples

Simple Text Editor

def simple_text_editor():
    """A very simple text editor."""
    filename = input("Enter filename to edit: ")
    
    # Try to load existing file
    try:
        with open(filename, "r") as file:
            content = file.read()
        print(f"Loaded file: {filename}")
    except FileNotFoundError:
        content = ""
        print(f"New file: {filename}")
    
    # Show current content
    print("\nCurrent content:")
    print("-" * 40)
    print(content)
    print("-" * 40)
    
    # Edit content
    print("\nEnter new content (type 'EOF' on a new line to finish):")
    new_content = []
    while True:
        line = input()
        if line == "EOF":
            break
        new_content.append(line)
    
    # Save or discard changes
    save = input("\nSave changes? (y/n): ").lower()
    if save == "y":
        with open(filename, "w") as file:
            file.write("\n".join(new_content))
        print(f"Changes saved to {filename}")
    else:
        print("Changes discarded")

# Uncomment to run the editor
# simple_text_editor()

CSV Data Processor

import csv

def process_csv_data(input_file, output_file):
    """Read CSV data, process it, and write results to a new CSV file."""
    
    # Read input data
    with open(input_file, "r", newline="") as infile:
        reader = csv.DictReader(infile)
        data = list(reader)
    
    print(f"Read {len(data)} rows from {input_file}")
    
    # Process data (simple example: calculate age in months)
    for row in data:
        try:
            age_years = int(row["Age"])
            row["Age_Months"] = age_years * 12
        except (ValueError, KeyError):
            row["Age_Months"] = "N/A"
    
    # Write output data
    with open(output_file, "w", newline="") as outfile:
        # Get all field names, including new ones
        fieldnames = list(data[0].keys())
        writer = csv.DictWriter(outfile, fieldnames=fieldnames)
        
        writer.writeheader()
        writer.writerows(data)
    
    print(f"Processed data written to {output_file}")

# Example usage
# process_csv_data("people.csv", "people_processed.csv")

Log File Analyzer

def analyze_log_file(log_file):
    """A simple log file analyzer that counts error types."""
    error_types = {}
    
    try:
        with open(log_file, "r") as file:
            line_count = 0
            error_count = 0
            
            for line in file:
                line_count += 1
                
                # Simple example: count lines containing "ERROR"
                if "ERROR" in line:
                    error_count += 1
                    
                    # Extract error type (simple example)
                    parts = line.split("ERROR:", 1)
                    if len(parts) > 1:
                        error_message = parts[1].strip()
                        # Get first few words as error type
                        error_type = " ".join(error_message.split()[:3])
                        
                        error_types[error_type] = error_types.get(error_type, 0) + 1
        
        # Print summary
        print(f"Log File Analysis: {log_file}")
        print(f"Total lines: {line_count}")
        print(f"Error lines: {error_count} ({error_count/line_count:.1%} of total)")
        
        if error_types:
            print("\nError Types:")
            for error_type, count in sorted(error_types.items(), key=lambda x: x[1], reverse=True):
                print(f"- {error_type}: {count} occurrences")
    
    except FileNotFoundError:
        print(f"Error: Log file '{log_file}' not found.")
    except Exception as e:
        print(f"Error analyzing log file: {e}")

# Example usage (create a sample log file first)
with open("sample.log", "w") as log:
    log.write("2025-04-15 12:30:45 INFO: Application started\n")
    log.write("2025-04-15 12:31:10 ERROR: Database connection failed\n")
    log.write("2025-04-15 12:31:15 INFO: Retrying connection\n")
    log.write("2025-04-15 12:32:20 ERROR: Database connection failed\n")
    log.write("2025-04-15 12:33:45 ERROR: Invalid user input\n")
    log.write("2025-04-15 12:35:12 INFO: User logged in\n")

# analyze_log_file("sample.log")

Best Practices for Input and Output

Console I/O Best Practices

# Example of good console I/O practices
def get_user_info():
    """Get and validate user information."""
    # Clear prompt with expected format
    name = input("Enter your name: ")
    while not name:
        print("Error: Name cannot be empty.")
        name = input("Enter your name: ")
    
    # Input validation with helpful error message
    while True:
        try:
            age = int(input("Enter your age (18-120): "))
            if 18 <= age <= 120:
                break
            print("Error: Age must be between 18 and 120.")
        except ValueError:
            print("Error: Please enter a valid number.")
    
    # Confirmation of input
    print("\nPlease confirm your information:")
    print(f"Name: {name}")
    print(f"Age: {age}")
    
    confirm = input("Is this correct? (y/n): ").lower()
    return name, age, confirm == 'y'

# get_user_info()

File I/O Best Practices

import os
from pathlib import Path

def safe_file_operations():
    """Example of best practices for file operations."""
    
    # Using pathlib for platform-independent paths
    data_dir = Path("data")
    input_file = data_dir / "input.txt"
    output_file = data_dir / "output.txt"
    
    # Create directory if it doesn't exist
    os.makedirs(data_dir, exist_ok=True)
    
    # Checking if file exists before reading
    if not input_file.exists():
        print(f"Warning: {input_file} does not exist")
        return
    
    # Create backup before modifying
    if output_file.exists():
        backup_file = output_file.with_suffix(".bak")
        output_file.rename(backup_file)
        print(f"Created backup: {backup_file}")
    
    try:
        # Processing large file efficiently
        with open(input_file, "r") as infile, open(output_file, "w") as outfile:
            for line in infile:  # Read line by line, not all at once
                # Process line
                processed_line = line.upper()
                outfile.write(processed_line)
        
        print(f"Processing complete: {input_file} → {output_file}")
    
    except Exception as e:
        print(f"Error during file processing: {e}")
        
        # Restore from backup if something went wrong
        if 'backup_file' in locals() and backup_file.exists():
            backup_file.rename(output_file)
            print(f"Restored from backup due to error")

# safe_file_operations()

Practice Exercises

Exercise 1: Enhanced Calculator

Create a calculator program that takes user input for two numbers and an operation, performs the calculation, and displays the result with proper formatting.

Exercise 2: File Copier

Write a program that copies the contents of one file to another, with these features:

Exercise 3: CSV Data Analyzer

Create a program that reads a CSV file containing tabular data (e.g., sales records, student scores) and produces a summary report with statistics like averages, totals, min/max values, etc.

Exercise 4: Simple Note-Taking App

Build a simple console-based note-taking application that allows users to:

Store the notes in a text or JSON file.

Exercise 5: Log Parser

Create a log parser that reads a log file, extracts important information (e.g., error messages, timestamps), and generates a summary report.

Wrapping Up and Next Steps

Today we've explored the fundamentals of input and output in Python, from basic console interaction to file operations and formatting. These skills form the foundation for creating interactive programs and processing data from external sources.

Key Takeaways

Where to Go from Here

  1. Explore more advanced file formats like XML, YAML, or Excel spreadsheets
  2. Learn about database I/O using Python's database modules (sqlite3, SQLAlchemy, etc.)
  3. Investigate network I/O for communicating with web services and APIs
  4. Dive into GUI programming for more sophisticated user interfaces
  5. Explore specialized I/O libraries for scientific computing and data analysis

Additional Resources

In our next session, we'll explore control flow in Python - how to make decisions, create loops, and control the execution of your programs.