Hello and welcome to the first lesson of your course on Data Engineering!
This course is designed to equip you with the skills needed for a professional data engineering role. We'll start with the foundational building blocks and progressively move towards building complex, cloud-native data systems.
This first module, "Professional Python for Data Engineering," focuses on writing clean, efficient, and robust Python code—a core competency for any data engineer. In this lesson, we will tackle the first learning outcome: working with core Python data structures and using comprehensions and generators for efficient processing.
Given your background in physics and data analytics, you've likely encountered Python's data structures before. Our goal here is to formalize that knowledge and introduce advanced techniques that are critical for handling the large datasets you'll encounter in data engineering, emphasizing performance and memory efficiency.
Let's begin.
1. Choosing the Right Tool: Core Data Structures
In data engineering, performance is key. The choice of data structure can have a significant impact on both the speed and memory usage of your data pipelines. Let's quickly review the fundamental Python data structures and their primary use cases in our field.
-
Lists: Ordered, mutable collections. They are versatile and easy to use.
- DE Context: Useful for holding a sequence of data points, like rows read from a CSV file before processing. However, checking for the existence of an item in a large list is slow (
O(n)complexity).
- DE Context: Useful for holding a sequence of data points, like rows read from a CSV file before processing. However, checking for the existence of an item in a large list is slow (
-
Tuples: Ordered, immutable collections. Once created, they cannot be changed.
- DE Context: Ideal for representing fixed records, such as a row from a database table
(user_id, name, timestamp), ensuring data integrity during processing.
- DE Context: Ideal for representing fixed records, such as a row from a database table
-
Dictionaries: Collections of key-value pairs (ordered since Python 3.7). They provide fast lookups, insertions, and deletions based on keys.
- DE Context: Essential for mapping and lookups. For example, creating a mapping from a product ID to its price, or representing semi-structured data like JSON objects. Lookups are very fast (average
O(1)complexity).
- DE Context: Essential for mapping and lookups. For example, creating a mapping from a product ID to its price, or representing semi-structured data like JSON objects. Lookups are very fast (average
-
Sets: Unordered collections of unique elements.
- DE Context: Their main strength is extremely fast membership testing (also average
O(1)complexity). They are perfect for tasks like finding all unique user IDs in a log file or checking if a particular event type has already been processed.
- DE Context: Their main strength is extremely fast membership testing (also average
The following article explains the performance implications of choosing the wrong data structure.
High-performance Python for Data Engineering
Please read the following section from the article 'High-performance Python for Data Engineering' on the Dagster blog. It provides a practical example comparing the performance of lists and sets for membership testing, a very common operation in data processing.
Read the subsection titled 'Data structures'. It starts with 'The right data structure...' and ends just before 'List comprehension and generator expressions'. Focus on the code examples that contrast checking for an item in a list versus a set.
This distinction between lists and sets is a classic example of how a simple design choice can dramatically affect pipeline performance, especially as data volume grows.
2. Efficient Collection Creation: Comprehensions
A common task in data pipelines is to transform one collection into another—for example, extracting a specific field from a list of records or creating a lookup table from raw data. While for loops can accomplish this, Python provides a more concise, readable, and often faster alternative: comprehensions.
To get a comprehensive overview of list, dictionary, and set comprehensions, please watch the following video.
Python Tutorial: Comprehensions - How they work and why you should be using them
This video from Corey Schafer's excellent Python series provides a clear, step-by-step guide to comprehensions. It compares them to traditional for-loops, which helps build a strong mental model.
Watch the video from the beginning until the timestamp 15:38. This covers list, dictionary, and set comprehensions, including how to add conditional logic and handle nested loops. The last part on generators can be skipped for now, as we'll cover it next.
Key Takeaways from the Video:
-
List Comprehensions:
[expression for item in iterable if condition]- Creates a new list by applying an expression to each item in an iterable.
- Example:
[user['id'] for user in users if user['country'] == 'CA']
-
Dictionary Comprehensions:
{key_expr: value_expr for item in iterable if condition}- Creates a new dictionary.
- Example:
{user['id']: user['name'] for user in users}
-
Set Comprehensions:
{expression for item in iterable if condition}- Creates a new set with unique elements.
- Example:
{user['country'] for user in users}
As you can see, the syntax is not only compact but also declarative—it describes what you want to create, rather than the step-by-step process of how to create it. This often leads to more "Pythonic" and maintainable code.
Test your understanding!
You have a list of dictionaries, where each dictionary represents a particle physics event with an event ID, energy in GeV, and a list of detected particles.
events = [
{'id': 101, 'energy_gev': 1500, 'particles': ['electron', 'muon']},
{'id': 102, 'energy_gev': 2200, 'particles': ['muon', 'tau']},
{'id': 103, 'energy_gev': 800, 'particles': ['electron']},
{'id': 104, 'energy_gev': 2500, 'particles': ['electron', 'muon', 'tau']}
]
Using comprehensions, write a single line of code for each of the following tasks:
- Create a list of event IDs for all events with an energy greater than 2000 GeV.
- Create a set of all unique particle types detected across all events.
- Create a dictionary that maps each event ID to its energy in TeV (Tera-electron volts), where 1 TeV = 1000 GeV.
Show answer
- List of high-energy event IDs:
high_energy_ids = [event['id'] for event in events if event['energy_gev'] > 2000] # Result: [102, 104] - Set of unique particles:
unique_particles = {particle for event in events for particle in event['particles']} # Result: {'electron', 'muon', 'tau'} # Note the nested loop structure within the comprehension. - Dictionary of event energy in TeV:
energy_in_tev = {event['id']: event['energy_gev'] / 1000 for event in events} # Result: {101: 1.5, 102: 2.2, 103: 0.8, 104: 2.5}
3. Memory-Efficient Processing: Generators
Comprehensions are great, but they have one potential drawback: they create the entire new collection in memory at once. This is fine for small datasets, but what if you need to process a file with billions of records? Loading all of that into a list would likely crash your program.
This is where generators come in. Generators are special iterators that "yield" items one at a time, without storing the entire sequence in memory. This concept is known as lazy evaluation and is fundamental to high-performance data engineering.
The next video explains generators, how they differ from list-returning functions, and their significant performance advantages.
Python Tutorial: Generators - How to use them and the benefits you receive
This video, also by Corey Schafer, introduces generators. It clearly demonstrates the 'yield' keyword and contrasts the memory footprint of a generator with a list, which is a crucial concept for processing large-scale data.
Watch the video from the beginning until timestamp 10:57. Pay close attention to: The difference between return in a regular function and yield in a generator function. How for loops automatically handle the StopIteration from a generator. The demonstration of memory and time performance differences when working with a large number of records.
Just as we have list comprehensions, we also have a similarly concise syntax for creating generators: generator expressions. They look almost identical to list comprehensions, but use parentheses () instead of square brackets [].
- List Comprehension:
[x*x for x in range(10)](creates a list in memory) - Generator Expression:
(x*x for x in range(10))(creates a generator object)
When you iterate over the generator expression, it calculates each value on the fly. This makes them incredibly memory-efficient.
The image below, taken from a performance analysis, starkly illustrates this difference. It compares the memory used to sum the squares of 10 million numbers using a list comprehension versus a generator expression.

In data engineering, you might use a generator expression to read and process a huge log file line by line, calculating an aggregate value (like a sum or count) without ever loading the whole file into RAM.
For a final read on these topics, the following article provides a good summary and places these concepts within the broader context of effective collection handling in Python.
Getting the most out of Python collections
This article, 'Getting the most out of Python collections', offers a concise review of comprehensions and generators.
Read the first two main sections: 'Create and filter Python collections with Comprehensions' and 'Iterating with Generators'. This will reinforce what you've learned from the videos.
Conclusion
In this lesson, we established a foundational understanding of how to work with data efficiently in Python. You've revisited core data structures through the lens of performance and learned two powerful techniques for processing data.
Key Takeaways:
- The choice of data structure (
list,set,dict) has major performance implications, especially for lookup and membership testing operations. - Comprehensions offer a readable, concise, and efficient way to create lists, sets, and dictionaries from existing iterables.
- Generators and generator expressions are essential for memory-efficient data processing. By using lazy evaluation, they allow you to handle datasets far larger than your available RAM.
These concepts are not just theoretical; they are practical tools you will use daily to build scalable and performant data pipelines.
Preview of the Next Lesson:
Now that you have the tools to manipulate data efficiently, our next lesson, "Write modular and reusable code using functions and classes," will focus on organizing your logic. We'll explore how to structure your code to make it maintainable, testable, and reusable—all critical attributes of professional-grade data engineering projects.