Professional Python for AI and RAG Engineering
Modern Pythonic Foundations
Beyond the Basics
You've learned the syntax, you can write loops, and you know what a dictionary is. Now it's time to start writing Python like a professional. 'Pythonic' code isn't just about getting the right answer; it's about writing code that is clear, efficient, and expressive. It leverages the language's features to solve problems elegantly.
Let's start with a simple but powerful tool: f-strings. They provide a concise way to embed expressions inside string literals. Instead of cumbersome concatenation or the .format() method, you can place variables and expressions directly into a string.
doc_id = "doc_789"
chunk_count = 15
# Old way
log_message = 'Processing document ' + doc_id + ' which has ' + str(chunk_count) + ' chunks.'
# Pythonic way with f-strings
log_message_f = f"Processing document {doc_id} which has {chunk_count} chunks."
print(log_message_f)
# Output: Processing document doc_789 which has 15 chunks.
F-strings are more than just convenient. They are also faster, making them the standard for string formatting in modern Python. You can even put expressions inside them, like f"Total cost: {price * 1.2:.2f}" to format a number.
Advanced Comprehensions
List comprehensions are a hallmark of Pythonic code, allowing you to create lists from other iterables in a single, readable line. You can move beyond simple transformations by adding conditions and even nesting them.
Imagine you have a list of text documents, each with some metadata. In a RAG pipeline, you might need to filter these documents before processing them. A list comprehension makes this task incredibly clean.
# A list of documents, each as a dictionary
documents = [
{"text": "The sky is blue.", "metadata": {"source": "wiki", "year": 2022}},
{"text": "An apple is a fruit.", "metadata": {"source": "news", "year": 2023}},
{"text": "Python is a language.", "metadata": {"source": "wiki", "year": 2021}}
]
# Get the text of all documents from the 'wiki' source from 2021 or later
filtered_texts = [
doc["text"]
for doc in documents
if doc["metadata"]["source"] == "wiki" and doc["metadata"]["year"] >= 2021
]
print(filtered_texts)
# Output: ['The sky is blue.', 'Python is a language.']
Dictionary comprehensions work on the same principle, but for creating dictionaries. You can swap keys and values, transform values, or filter items from an existing dictionary.
# Create a dictionary mapping document IDs to text length
texts = {"doc1": "Hello world", "doc2": "This is a test", "doc3": "Python is fun"}
text_lengths = {doc_id: len(text) for doc_id, text in texts.items()}
print(text_lengths)
# Output: {'doc1': 11, 'doc2': 14, 'doc3': 13}
While powerful, complex nested comprehensions can become hard to read. If a comprehension spans more than two or three lines, a traditional
forloop might be more maintainable.
For very large datasets, creating a full list in memory can be inefficient. This is where generator expressions come in. Syntactically, you just replace the square brackets [] with parentheses (). Instead of building the entire list, a generator expression creates an iterator that yields one item at a time. This is incredibly memory-efficient for tasks like processing massive text files line-by-line.
# Assume 'huge_log_file' is a file with millions of lines
# Inefficient: reads the whole file into memory first
lines_list = [line.strip() for line in huge_log_file]
# Memory-efficient: creates a generator to process one line at a time
lines_generator = (line.strip() for line in huge_log_file)
# You can then iterate over the generator
for line in lines_generator:
# process each line without holding all of them in memory
pass
Unpacking and Aligning Data
Often, you'll have data in a list or dictionary that you need to pass into a function. The unpacking operators, * and **, provide a clean way to do this.
The single-asterisk * operator unpacks iterables like lists and tuples into positional arguments. The double-asterisk ** operator unpacks dictionaries into keyword arguments. This is especially useful for merging dictionaries or dynamically calling functions.
def create_metadata(doc_id, source, author):
return {"id": doc_id, "source": source, "author": author}
# Using ** to unpack a dictionary
metadata_dict = {"doc_id": "xyz-123", "source": "internal", "author": "Alice"}
metadata_obj = create_metadata(**metadata_dict)
# Using ** to merge two dictionaries (Python 3.5+)
base_data = {"source": "web", "timestamp": 1678886400}
extra_data = {"author": "Bob", "status": "draft"}
combined_data = {**base_data, **extra_data}
print(combined_data)
# Output: {'source': 'web', 'timestamp': 1678886400, 'author': 'Bob', 'status': 'draft'}
When processing data for AI models, you often have parallel lists. For example, one list of text chunks and another list of their corresponding embedding vectors. The zip function is the perfect tool for iterating over them in sync.
text_chunks = ["First chunk of text.", "Second chunk.", "And the third."]
embeddings = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] # Simplified vectors
for chunk, vector in zip(text_chunks, embeddings):
print(f"Chunk: '{chunk}' -> Vector: {vector}")
# Output:
# Chunk: 'First chunk of text.' -> Vector: [0.1, 0.2]
# Chunk: 'Second chunk.' -> Vector: [0.3, 0.4]
# Chunk: 'And the third.' -> Vector: [0.5, 0.6]
If you need the index as well, combine zip with enumerate. The enumerate function adds a counter to an iterable. It's cleaner than manually creating and incrementing an index variable.
ids = ["id_a", "id_b", "id_c"]
texts = ["Text A", "Text B", "Text C"]
for i, (doc_id, text) in enumerate(zip(ids, texts)):
print(f"Processing item {i}: {doc_id} - {text}")
# Output:
# Processing item 0: id_a - Text A
# Processing item 1: id_b - Text B
# Processing item 2: id_c - Text C
Finally, let's look at the assignment expression operator :=, informally known as the due to its appearance. Introduced in Python 3.8, it allows you to assign a value to a variable as part of a larger expression. This can simplify some loops, particularly when you need to check a condition and then use the value that satisfied it.
# Pre-Python 3.8, processing lines from a file
while True:
line = file.readline()
if not line:
break
# process(line)
# With the walrus operator
while (line := file.readline()):
# process(line)
Using these tools will make your code more efficient, readable, and professional. They are the building blocks for writing high-quality Python that others can easily understand and maintain.