No history yet

Advanced Python Scripting

Writing Professional Python

Moving beyond basic scripts means writing code that is not just correct, but also efficient, readable, and scalable. This is often called writing 'Pythonic' code. It involves using the language's features as they were intended, leading to cleaner and more effective programs. We'll explore several advanced patterns that are hallmarks of professional Python development.

Handling Many Tasks at Once

Traditional programs run one instruction at a time. If a task has to wait for something, like a response from a web server, the entire program pauses. This is called blocking I/O, and it's a major bottleneck in applications that handle network requests or file operations.

Asynchronous programming solves this. Instead of waiting, the program can switch to another task. When the original task is ready to continue, the program switches back. Python's built-in library for this is which uses the async and await keywords to manage this process.

An async function, called a coroutine, can be paused and resumed. When you await a function call, you're telling the event loop, "I'm going to be waiting here for a result. Feel free to run something else in the meantime."

import asyncio
import time

# A mock function that simulates a network request
async def fetch_data(delay):
    print(f"Starting fetch... will take {delay} seconds.")
    await asyncio.sleep(delay) # Non-blocking wait
    return f"Data fetched after {delay} seconds."

async def main():
    start_time = time.time()

    # Run two fetch tasks concurrently
    task1 = asyncio.create_task(fetch_data(2))
    task2 = asyncio.create_task(fetch_data(3))

    result1 = await task1
    result2 = await task2

    print(result1)
    print(result2)

    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds.")

# To run the top-level async function
asyncio.run(main())

# Expected output shows a total time of ~3 seconds, not 5.

Cleaner Code with Decorators

A decorator is a function that takes another function as an argument, adds some functionality, and returns the modified function. This allows you to extend the behavior of functions without permanently altering their source code. It's a clean way to add logging, timing, or access control logic.

This pattern is possible because in Python, functions are first-class objects. You can pass them around, return them from other functions, and assign them to variables, just like any other data type.

import time

# This is a decorator
def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} ran in: {end_time - start_time:.4f} sec")
        return result
    return wrapper

@timer
def do_something(n):
    """A simple function that takes some time to run."""
    total = 0
    for i in range(n):
        total += i
    return total

do_something(1000000)

# The @timer syntax is shorthand for:
# do_something = timer(do_something)

Another powerful tool for managing resources is the context manager. You've likely used one with the with statement when opening files: with open('file.txt', 'r') as f:. This pattern guarantees that a resource, like a file handle or a network connection, is properly cleaned up afterward, even if errors occur within the block.

Lesson image

Scalable and Reliable Code

When processing large amounts of data, memory efficiency becomes critical. List comprehensions are a concise way to create lists, but they load the entire list into memory at once. For very large datasets, this can be a problem.

solve this by creating iterators that yield one item at a time. A generator expression looks like a list comprehension but uses parentheses instead of square brackets. It doesn't build the whole collection in memory, making it far more scalable.

A list comprehension: [x*x for x in range(1000)] A generator expression: (x*x for x in range(1000))

As projects grow, keeping track of data types can become difficult. Python is dynamically typed, which means you don't have to declare a variable's type. While flexible, this can lead to runtime errors that are hard to find.

Type hinting allows you to annotate your code with expected types. These hints don't change how the code runs, but they can be checked by external tools like to catch type-related bugs before you ever run the program. This makes your code more robust and self-documenting.

# This function has type hints
def greet(name: str) -> str:
    return f"Hello, {name}"

# MyPy will catch this error before you run the code!
greet(123)  # Argument 1 to "greet" has incompatible type "int"; expected "str"

Finally, professional projects require disciplined dependency management. While pip and requirements.txt are standard, the ecosystem is evolving. A modern tool called is gaining popularity for its incredible speed.

Developed by the creator of the ruff linter, uv is an extremely fast Python package installer and resolver written in Rust. It can replace pip and venv for creating virtual environments and installing packages, often orders of magnitude faster. This significantly speeds up development workflows, especially in projects with many dependencies.

# Instead of:
# python -m venv .venv
# source .venv/bin/activate
# python -m pip install -r requirements.txt

# With uv, you can do:

# Create and activate a virtual environment
uv venv
source .venv/bin/activate

# Install dependencies from a requirements file
uv pip sync requirements.txt

These techniques—asynchronous programming, decorators, context managers, efficient data handling, type hinting, and modern tooling—are the building blocks for writing professional, high-quality Python code.