By Leonardo Zeaiter

Part 2 of 5 in a free introductory series, laying the groundwork for my forthcoming book: *Python Beyond Syntax: Advanced Topics for the C# Developer*.


It's worth noting that everything covered in this series is intentionally a quick, practical introduction, enough to get you reading and writing basic Python comfortably, not a complete treatment of any single topic. Think of this series as learning to hold a conversation; the book is where you become fluent. Python Beyond Syntax: Advanced Topics for the C# Developer takes everything you already know from years of C# and uses it as the map for learning Python properly: not just syntax, but the reasoning behind async, memory management, object-oriented design, generators and the idioms that make Python code actually look like Python. Each topic is explored in both languages side by side, so you always know exactly where your existing instincts hold and where they'll mislead you. And because Python is the language behind nearly every AI tool and library worth knowing, this isn't just a second language, it's your fastest path to being dangerous with AI development without starting from zero.


Where We Left Off

In Article 1, you got comfortable reading Python: indentation, data types, variables, naming conventions, if/elif and loops. That's the grammar. This article is the vocabulary: the collections you'll actually use constantly, in almost every piece of Python you write.

You already think in lists and key-value lookups every day in C#. What's ahead is mostly a lighter, more flexible syntax for things you already know how to use.


1. Lists

Python's list is close to List<T> in C#; it is an ordered (thus indexed) and mutable collection that allows duplicates. The difference is that a Python list has no declared element type. It can hold anything, including a mix of types in the same list, though in practice you'll usually keep them uniform out of habit and readability.

scores = [95, 87, 72, 100]  # this is a list of int

Enter fullscreen mode Exit fullscreen mode

To create a list, just use square brackets with comma-separated values; no new List<int> { ... } ceremony required.

Common operations

scores = [95, 87, 72, 100]

print(type(scores) == list)  # True

len(scores)          # 4 - returns the number of items 
                     # works on all four collection types

scores.append(60)    # adds 60 to the end of the list: 
                     # [95, 87, 72, 100, 60]

scores[0]     # 95 - indexing works just like C#'s array/list indexing
scores[-1]    # 60 - negative indexing counts from the end
              # this returns the last element

scores[-2]    # 100 - the second last item

sub_scores = scores[1:3]    # slicing: a sub-list from index 1 up to 
                            # (not including) index 3 

print(type(sub_scores) == list)  # True - the result is itself a list
print(sub_scores)                # [87, 72]

Enter fullscreen mode Exit fullscreen mode

A list can be mutated using just the indexer. This is a powerful feature as it allows you to insert, update and remove easily without memorizing different functions.

scores = [95, 87, 72, 100]

# replace 87 and 72 by 77, 76 and 75
scores[1:3] = [77, 76, 75]  # translates to: start at index 1 then 
                            # replace 2(3-1) elements with 77, 76, and 75

print(scores)               # output: [95, 77, 76, 75, 100]

scores = [95, 87, 72, 100]  # reset scores

# insert 98 before 100 i.e. at index 3
scores[3:3] = [98]    # this translates to: start at index 3 then 
                      # replace 0(3-3) elements with 98

# this could have been achieved using the insert(): scores.insert(3, 98)

scores = [95, 87, 72, 100]  # reset scores

scores[2:4] = []  # this translates to: start at index 2, replace 2(4-2)
                  # elements with nothing i.e. delete them

print(scores)     # output: [95, 87]

Enter fullscreen mode Exit fullscreen mode

Everything in Python is an object including primitive types like int, float, bool etc. Objects act almost the same way as their C# counterparts.

scores = [95, 87, 72, 100]

values = scores  # now both scores and values refer to the same object in memory

print(id(values) == id(scores))  # returns True indicating both objects refer to
                                 # the same address 

scores[1] = 90   # modify the second element of scores; values is also affected 
                 # as both point to the same list

print(scores)    # output: [95, 90, 72, 100]
print(values)    # output: [95, 90, 72, 100]

scores_clone = scores.copy()           # create a clone of the scores list

print(scores == scores_clone)          # True, both lists have the same content 
                                       # this is different in C#

print(id(scores) == id(scores_clone))  # False, they are different objects in memory

scores_clone[1] = 85                   # modify the second element of the clone; 
                                       # original scores is unaffected

print(scores)                          # output: [95, 90, 72, 100]
print(values)                          # output: [95, 90, 72, 100]
print(scores_clone)                    # output: [95, 85, 72, 100]  

Enter fullscreen mode Exit fullscreen mode

A striking difference with C# is when comparing two lists having the same content but referring to two different objects in memory; in C#, the == operator returns false while in Python, and as we noticed in the example above, the result was True. The fact of having both objects referring to different areas in memory was detected through comparing the ids of both lists on the line beneath. When we deep dive into the dunder methods of Python, we'll learn how we can override this behavior.


2. Dictionaries

Python's dict is your Dictionary<TKey, TValue>. Same core idea: unique keys mapped to values, fast lookup by key. Dictionaries are not ordered, do not allow duplicate keys and, like lists, are mutable.

user = {"name": "John", "age": 48, "active": True}

Enter fullscreen mode Exit fullscreen mode

No new Dictionary<string, object> declaration needed; just curly braces and keys/values that can be of different types within the same dictionary without any special handling.

Common operations

user = {"name": "John", "age": 48, "active": True}

len(user)                   # 3 - number of items
user["name"]                # "John" - direct key access, like C#'s indexer 
                            # if no entry with key, raise error
user.get("email", "N/A")    # "N/A" - safer access: if the key is missing
                            # return the second argument
"age" in user               # True - checks whether a key exists, no TryGetValue needed
user.keys()                 # dict_keys(['name', 'age', 'active']) - all the keys, for iterating
user.values()               # all the values
user.items()                # more on that later

Enter fullscreen mode Exit fullscreen mode

user.get("email", "N/A") is worth pausing on; it's a one-line equivalent of C#'s TryGetValue pattern, returning a fallback value instead of throwing or requiring an out parameter. It's one of the small conveniences that makes Python dictionaries feel a bit less ceremonious to work with day to day.

user = {
   "id": 1,
   "name": "John Doe",
   "salary": 15_000
}

print(type(user) == dict)  # True

print(user["id"])   # output: 1

print(user["age"])  # raise an error (KeyError) - can you use the safer get() instead

# loop through the dictionary
for key in user:
    print(f"{key}: {user[key]}")  

# output: 
#        id: 1
#        name: John Doe 
#        salary: 15000 

# a more common way to loop through a dictionary
for key, value in user.items():  # unpack the key and value from each item
    print(f"{key}: {value}")   


if("name" in user):
    print("Name exists in the dictionary")  
    # output: Name exists in the dictionary 

print(user.get("salary", None))            
# output: 15000 - no KeyError even if the key is not present

john = user.copy()                          
# create a clone of the user dictionary; changing john will not affect user

Enter fullscreen mode Exit fullscreen mode


3. Tuples

A tuple is an ordered, fixed-size, immutable sequence. The closest C# cousin is ValueTuple (the (int, string) syntax), but tuples are far more common in everyday Python, especially for returning multiple values from a function without declaring a class or a named tuple type first.

point = (3, 7)  # parentheses with comma-separated values 
                # can have any number of values, not just two

Enter fullscreen mode Exit fullscreen mode

Once created, you can't reassign any individual element; typing something like point[0] = 5 raises an error. Remember that among the four common collections of Python, tuples are the only immutable entities; create one and you'll never be able to modify its content before first destroying it.

Common operations

point = (3, 7)

len(point)    # 2 - same len() function as lists and dicts

point[0]      # 3 - indexing works the same way as lists
point[1]      # 7

x, y = point  # unpacking: x = 3, y = 7, in one line

Enter fullscreen mode Exit fullscreen mode

It's a common pattern in Python, to have a function return multiple values packed into a tuple; the caller can then unpack the result into individual variables as in the below code:

def get_coordinates():
    return (3, 7)  # parentheses can be removed here

x, y = get_coordinates()
print(x,y)  # print 3 7

Enter fullscreen mode Exit fullscreen mode

No out parameters, no wrapper class needed just to hand back two values at once.


4. Sets

A set is an unordered collection of unique values, Python's equivalent of C#'s HashSet<T>. No duplicates, no guaranteed order, and it's built for fast membership testing.

tags = {"python", "csharp", "ai", "python"}  
# no errors even though python is included twice; duplicates are just ignored

print(tags)  # {'python', 'ai', 'csharp'}

Enter fullscreen mode Exit fullscreen mode

Curly braces with comma-separated values, no colons; that's what distinguishes a set literal from a dict literal at a glance.

Common operations

tags = {"python", "csharp", "ai"}

len(tags)               # 3 - number of items
"python" in tags        # True - fast membership check, the main reason to use a set
tags.add("dotnet")      # adds a new item: {"python", "csharp", "ai", "dotnet"} 
                        # no error if the item is already in the set
tags.remove("ai")       # removes "ai" from the set 
tags.remove({"react"})  # KeyError is raised because react is not in the set

if "react" in tags:
    tags.remove("react")   # safe way to remove an item that may not be present

Enter fullscreen mode Exit fullscreen mode

As in C#, set operations include intersection, union, difference and symmetric difference.

john_cars = {"Mercedes", "BMW", "Audi", "Toyota"}
edmond_cars = {"Honda", "Ford", "Toyota", "Chevrolet"}

# Common cars between John and Edmond: {'Toyota'}
common_cars = john_cars.intersection(edmond_cars)
print("Common cars between John and Edmond:", common_cars) 

# All cars owned by John and Edmond: 
# {'Toyota', 'Ford', 'Mercedes', 'Audi', 'Chevrolet', 'Honda', 'BMW'}
all_cars = john_cars.union(edmond_cars)
print("All cars owned by John and Edmond:", all_cars)  

# Cars owned by John but not by Edmond: 
# {'Audi', 'Mercedes', 'BMW'}
# order matters; (A - B) != (B - A)
john_unique_cars = john_cars.difference(edmond_cars)  
print("Cars owned by John but not by Edmond:", john_unique_cars)

# Cars owned by Edmond but not by John: 
# {'Honda', 'Ford', 'Chevrolet'}
edmond_unique_cars = edmond_cars.difference(john_cars)  
print("Cars owned by Edmond but not by John:", edmond_unique_cars)

# Cars owned by either John or Edmond but not both: 
# {'Honda', 'Ford', 'Chevrolet', 'Audi', 'Mercedes', 'BMW'}
unique_cars = john_cars.symmetric_difference(edmond_cars)
print("Cars owned by either John or Edmond but not both:", unique_cars)

Enter fullscreen mode Exit fullscreen mode

You'll probably use sets less often than lists or dicts, but when you need to ignore duplicates or quickly check if a value exists, sets will be the right tool; this is exactly the same reasoning that would point you toward HashSet<T> in C#.


What You Can Already Do

Between Article 1 and this one, you can now read and write Python code that stores data, loops through it and makes decisions. That's the working core of the language.

In the next article, you'll explore functions, lambdas, variable scoping and error handling. You'll also learn about list comprehension.

If you’d like early access to my forthcoming book: Python Beyond Syntax: Advanced Topics for the C# Developer, kindly sign up at https://tally.so/r/9q5l2Q.