
Introduction Link to heading
Welcome back to the series! In the first article we took a broad look at Python and its key differences with C#, covering syntax, modules, and exception handling. In the second, we explored object-oriented programming in Python. This time around, we are going to focus on one of the areas where Python truly shines: collections and list comprehensions. If you’re familiar with LINQ queries and have wrestled with generic collections in C#, you may find Python’s approach to data structure manipulation more direct and expressive.
Python’s Built-in Collection Types Link to heading
One of the first things that pleasantly surprises developers coming from C# is that Python ships with a rich set of collection types built right into the language. The four main collection types are:
- list: an ordered, mutable collection of items. The closest equivalent to C#’s
List<T>. - tuple: an ordered, immutable collection of items. A good choice for fixed, read-only sequences.
- dict: an unordered (insertion-ordered since Python 3.7) collection of key-value pairs. Python’s answer to
Dictionary<TKey, TValue>. - set: an unordered collection of unique items. Corresponds to C#’s
HashSet<T>.
We will walk through each with the obligatory C# comparison alongside, starting with the most commonly used of the four.
Lists Link to heading
The Python list is your go-to collection type, and is broadly equivalent to List<T> in C#. The syntax is considerably more succinct:
// C#
var fruits = new List<string> { "Banana", "Pear", "Apple" };
fruits.Add("Mango");
Console.WriteLine(fruits[0]); // Banana
Console.WriteLine(fruits.Count); // 4# Python
fruits = ["Banana", "Pear", "Apple"]
fruits.append("Mango")
print(fruits[0]) # Banana
print(len(fruits)) # 4A few immediate observations: Python lists require no type declaration, and they can hold items of any type and freely mix them, though mixing types is generally not advisable for maintainability. Instead of a .Count property, Python uses the built-in function len(), which works consistently across virtually every collection type. And append() is the equivalent of Add().
Python lists also support a particularly handy feature called slicing, which allows you to access a portion of a list using the notation list[start:end]:
fruits = ["Banana", "Pear", "Apple", "Mango"]
print(fruits[1:3]) # ['Pear', 'Apple']
print(fruits[:2]) # ['Banana', 'Pear']
print(fruits[2:]) # ['Apple', 'Mango']
print(fruits[-1]) # MangoThe last example is worth zooming in: negative indices count from the end of the list, so -1 gives you the last element, -2 the second to last, and so on. C# has no clean equivalent for this without calling Last() from LINQ or computing the index from Count.
Tuples Link to heading
Tuples are similar to lists in most respects, but with one fundamental difference: they are immutable. Once a tuple is created, its contents cannot be changed. They are defined using parentheses rather than square brackets:
coordinates = (36.380098, -6.219901)
# coordinates[0] = 99 # TypeError: 'tuple' object does not support item assignmentTuples are a good fit for representing fixed collections of related values: coordinates, RGB colour values, database rows and so on. They also support a rather elegant feature called unpacking, allowing you to assign a tuple’s values to individual variables in a single expression:
lat, lon = coordinates
print(lat) # 36.380098
print(lon) # -6.219901This is considerably more readable than accessing each element by index, and it is used widely in Python. We will see more unpacking throughout this series, as it comes up naturally across a variety of usages.
Dictionaries Link to heading
Python dictionaries map directly to C#’s Dictionary<TKey, TValue> and should feel conceptually familiar. Keys must be unique and immutable (strings, numbers, and tuples all qualify; lists do not), while values can be anything at all.
// C#
var capitals = new Dictionary<string, string>
{
{ "France", "Paris" },
{ "Germany", "Berlin" },
{ "Spain", "Madrid" }
};
Console.WriteLine(capitals["France"]); // Paris
capitals["Italy"] = "Rome";# Python
capitals = {
"France": "Paris",
"Germany": "Berlin",
"Spain": "Madrid"
}
print(capitals["France"]) # Paris
capitals["Italy"] = "Rome"Accessing a key that does not exist in C# throws a KeyNotFoundException; in Python, it raises a KeyError. The safe alternative is the .get() method, which returns None (or a default value you specify) if the key is not found, a neat parallel to TryGetValue in C#:
print(capitals.get("Portugal")) # None
print(capitals.get("Portugal", "N/A")) # N/ASets Link to heading
Sets work exactly as you might expect coming from HashSet<T>: unordered, no duplicates, and optimised for membership tests and set operations. They are created using curly braces (without key-value pairs) or the built-in set() constructor:
primes = {2, 3, 5, 7, 11}
evens = {2, 4, 6, 8, 10}
print(primes & evens) # Intersection: {2}
print(primes | evens) # Union: {2, 3, 4, 5, 6, 7, 8, 10, 11}
print(primes - evens) # Difference: order may vary, e.g. {3, 5, 7, 11}The operator syntax for set operations is simpler than that of C#, which requires calling IntersectWith, UnionWith, and ExceptWith. Bear in mind that sets and dictionaries both use curly braces. An empty set must be created with set(), not {}, as the latter creates an empty dictionary.
Iterating Over Collections Link to heading
Iterating over a list in Python will feel immediately familiar, though a little more concise:
// C#
foreach (var fruit in fruits)
{
Console.WriteLine(fruit);
}# Python
for fruit in fruits:
print(fruit)When you need both the index and the value (something that in C# usually means either a for (int i = 0; …) loop or some LINQ gymnastics) Python provides the built-in enumerate() function:
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")0: Banana
1: Pear
2: Apple
3: MangoYou will have noticed the f-string syntax above: f"…" with variables embedded directly inside curly braces. F-strings, available since Python 3.6, are the modern idiomatic replacement for the .format() calls used in the earlier articles in this series, and work very similarly to C#’s string interpolation with the $ prefix. We will use them throughout the rest of the series.
Another useful built-in for iteration is zip(), which lets you iterate over two or more collections simultaneously, analogous to Zip() from LINQ:
cities = ["London", "Paris", "Berlin"]
populations = [8982000, 2161000, 3645000]
for city, population in zip(cities, populations):
print(f"{city}: {population:,}")London: 8,982,000
Paris: 2,161,000
Berlin: 3,645,000Iterating over a dictionary also deserves a mention. The most common pattern uses .items(), which yields key-value tuples that we can unpack directly:
for country, capital in capitals.items():
print(f"{country}: {capital}")France: Paris
Germany: Berlin
Spain: Madrid
Italy: RomeYou can also iterate over keys alone with .keys() or values alone with .values(). Iterating the dictionary directly (without calling any method) yields just the keys by default.
List Comprehensions Link to heading
And now we’re talking! List comprehensions feature a concise, expressive syntax for building new lists from existing iterables, and they are one of the most pythonic constructs. The general form is simply an expression followed by a for clause inside square brackets, with an optional if condition to filter results:
# [expression for item in iterable]
# [expression for item in iterable if condition]Let’s look at a practical comparison. Suppose we want to take a list of numbers and produce a new list of their squares:
// C#
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var squares = numbers.Select(n => n * n).ToList();# Python
numbers = [1, 2, 3, 4, 5]
squares = [n * n for n in numbers]Now let’s add a filter to only keep squares of even numbers:
// C#
var evenSquares = numbers.Where(n => n % 2 == 0)
.Select(n => n * n)
.ToList();# Python
even_squares = [n * n for n in numbers if n % 2 == 0]The Python version reads almost like a plain English sentence, which very much the point. Bear in mind that list comprehensions are not purely syntactic sugar; they are also generally faster than an equivalent explicit for loop in Python, as the iteration is handled closer to the C layer internally. For transforming and filtering collections, they are almost always the right tool.
It is possible to nest list comprehensions, but try to keep readability in mind. Deeply nested comprehensions quickly become harder to follow than a conventional loop and rather defeat the purpose. A single level of nesting, such as flattening a list of lists, is typically where the readability balance holds up well:
nested = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [item for sublist in nested for item in sublist]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]Dictionary Comprehensions Link to heading
The same idea extends naturally to dictionaries, with equally readable results:
# Scale every score up by 10%
scores = {"Alice": 85, "Bob": 92, "Charlie": 78}
scaled = {name: round(score * 1.1) for name, score in scores.items()}
print(scaled) # {'Alice': 94, 'Bob': 101, 'Charlie': 86}
# Invert a dictionary: swap keys and values
inverted = {v: k for k, v in scores.items()}
print(inverted) # {85: 'Alice', 92: 'Bob', 78: 'Charlie'}There are also set comprehensions using curly braces without key-value pairs, though they come up less frequently in practice than their list and dictionary counterparts.
*args and **kwargs Link to heading
We noted in the first article that Python does not support function overloading in the C# sense. The Pythonic alternative is a combination of optional arguments, *args, and **kwargs.
Optional arguments you have already seen: simply provide a default value in the function signature and the caller may omit that argument. *args goes further and allows a function to accept any number of positional arguments, collecting them all into a tuple. **kwargs does the equivalent for keyword arguments, collecting them into a dictionary.
def greet(*names):
for name in names:
print(f"Hello, {name}!")
greet("Alice")
greet("Alice", "Bob", "Charlie")Hello, Alice!
Hello, Alice!
Hello, Bob!
Hello, Charlie!def display_info(**details):
for key, value in details.items():
print(f"{key}: {value}")
display_info(name="Alice", age=30, city="London")name: Alice
age: 30
city: LondonThe names args and kwargs are purely conventional; what matters are the single and double asterisks respectively. You can combine them all in a single function signature, as long as you keep the order: regular positional parameters, then *args, then any explicit keyword parameters, and finally **kwargs.
This gives Python functions a degree of flexibility that C# can achieve through method overloading or the params keyword, but with considerably less boilerplate. It also underpins some powerful patterns further down the line, such as decorator factories and forwarding wrappers.
In the next article, we will be tackling one of the more interesting differences between the two languages: inheritance in Python, including multiple inheritance, something Python supports natively and that C# deliberately prevents at the class level. Thanks for reading!