The Basics (variables, expressions, statements,
comments)
Python is a dynamically-typed, interpreted language. This basically means data types of
variables don't have to be specified beforehand, the variable holds whatever data type is
assigned to it.
A variable is a named location in memory that holds a value. They allow you to store data that
can be used and modified throughout your program.
Creating variables
Variables in Python are created using the = operator. Here are some examples of variable
creation:
a=2
string = 'Hello world'
a, b = 1, 2
a, b, c = [1, 2, 3] (unpacking)
An expression is a combination of values (or variables) and operators that are evaluated to
produce another value.
They are used to perform calculations, manipulate data or evaluate conditions. The result of an
expression is always a value.
Examples:
a=5+3
len("hello")
a, b = 2, 4 ** 2
A statement is a complete unit of execution. They may include one or more expressions but
they represent a complete command to the interpreter. Examples:
Assignment statement: x = 5 * 9
Calling a function: print("Hello. Is it me you're looking for?")
Basic data types
Python has several basic (primitive) data types that are used to store and manipulate data. The
most common ones are:
1. Integers ( int ): represent positive or negative whole numbers e.g. 1, -100, etc.
2. Floating point numbers ( float ): represent positive or negative decimal numbers e.g. 3.14,
-100.9 etc.
3. Strings ( str ):
represents text (sequences of characters)
Strings are enclosed in single quotes ( ' ), double quotes ( " ), or triple quotes ( ''' or
""" ) for multi-line strings.
greeting = "Hello"
name = 'Alice'
multiline = """This is a
multi-line string."""
4. Booleans ( bool ): True or False , used for conditional statements and logic operations.
5. None Type ( None ): represents the absence of a value or a null value. Often used as a
placeholder to indicate that a variable has no value: result = None
6. Complex numbers ( complex ): represents complex numbers with a real part and an
imaginary part. The imaginary part is denoted by a j e.g. 3 + 4j : z = 3 + 4j
The string (str)
Strings are one of the most used data types and definitely the most understandable so it makes
sense for them to have their own section.
We are not going to go over all the methods the string provides, just some important ones:
Getting the length of a string: len(string)
Checking the content of a string (alphabet, alphanumeric, numeric): string.isalpha() ,
string.isalnum() , string.isdigit()
Changing the case of a string: string.upper() , string.lower()
Reversing a string: string[::-1] . This is also how you would reverse a list or a tuple.
Sorting a string (alphabetically): sorted(string) or sorted(string, reverse=True) . This
returns a list of all the items in the string in alphabetical order.
Complex data types
Complex types are data types that are composed of one or more primitive types. They
implement data structures that provide different storage constraints and data access methods.
The most common complex data types are lists, tuples, sets and dictionaries.
Lists and tuples
Lists and tuples are both like arrays in that they are ordered collections of different elements.
But unlike traditional arrays, they can store elements of different types
Definition
To create a list, we use the [] (angle brackets). A list can be created empty or with values
already in it.
my_list = []
my_list = [1,2,3, "four"]
To create a tuple, we use the () (parentheses). A tuple should always be created with values in
it. (More on that later)
my_tuple = (1, 2, 3, "four")
Accessing elements
Items are stored sequentially and can be stored based on their index (basically the number of
items before the element you're trying to access).
To access 3 in my_list above, we use the expression my_list[2] , the same notation for the
tuple: my_tuple[2] .
NB: to get the length of a list or tuple use the len() function e.g. len(my_tuple)
How are lists different from tuples
So far lists and tuples are looking like identical twins. So how are they different? Immutability
Try modifying the value at a particular index of the list (e.g. my_tuple[1] = 10 ). What do
you observe?
Try modifying the value at a particular index of the tuple (e.g. my_tuple[1] = 10 ). What do
you observe?
Immutability
An immutable object is one whose state (structure) cannot be changed after it has been
modified.
Tuples are immutable while lists are mutable. That's why modifying the value at an index of the
tuple raises an error.
Now armed with this knowledge, look at the following code and pick one of the options
tup = (1, 2, 3)
tup[2] = 4
a = (1, 2, [3, 4, 5])
a[2][1] = 6
The above code:
A. They both will raise an error
B. The first will not raise an error and the second will raise an error
C. The first raises an error and the second doesn't
D. None of them will raise an error
Ok now type the code in the Python shell and observe. Was your chosen answer correct?
Use cases
Immutability:
Use a tuple when you want to ensure that the collection of items should not be modified
after creation.
Use a list if you need to add, remove or modify items.
coordinates = (10, 20) # Coordinates of a point in space
Semantic meaning
Use a tuple to represent a fixed structure or a "record" with a specific meaning for each
element. Tuples are often used to group heterogeneous data (data of different types),
whereas lists are typically used for homogeneous data (data of the same type).
# tuples representing student records (name, age, gpa)
esther = ("Esther", 21, 2.6)
yonta = ("Yonta", 23, 0.7)
# list representing a collection of students
students = [esther, yonta]
Use a tuple if performance is a priority, as tuples are generally more memory-efficient and
faster than lists, especially when you don't need to modify the data.
Use a tuple when you need a collection that can be used as a key in a dictionary or added
to a set. Tuples are hashable if they contain only hashable elements, while lists are not
hashable.
Sets
A set in Python is an unordered collection of unique elements.
It offers set operations like union, intersection, and difference.
Sets are mutable, but the elements they contain must be immutable.
Definition
From scratch: You can create an empty set using the set() function (note: {} creates a
dictionary, not a set).
my_set = set() # Empty set
Add elements to it using the `add` method of the set
my_set.add("New element")
From an iterable: You can pass an iterable like a list, tuple, or string to the set() function
to create a set.
From a list
my_set = set([1, 2, 3, 4, 4, 5])
print(my_set) # Output: {1, 2, 3, 4, 5}
From a tuple
my_tuple_set = set((1, 2, 2, 3, 4))
print(my_tuple_set) # Output: {1, 2, 3, 4}
From a string
my_string_set = set("hello")
print(my_string_set) # Output: {'e', 'h', 'o', 'l'}
```
Set literal: You can also create a set using curly braces {} with elements separated by
commas.
python my_set = {1, 2, 3, 4, 5}
Accessing elements
Sets do not support indexing, slicing, or direct access to elements by position like lists or tuples.
However, there are a few ways to access elements in a set:
1. Iterating over the set:
You can access elements by iterating over the set with a loop.
my_set = {1, 2, 3, 4, 5}
for element in my_set:
print(element)
Output might look like this (order may vary):
1
2
3
4
5
2. Using the in operator:
You can check if a specific element is present in the set using the in operator.
my_set = {1, 2, 3, 4, 5}
if 3 in my_set:
print("3 is in the set")
3. Converting to a list/tuple:
If you need to access elements by index (or perform slicing), you can convert the set into a
list or tuple first.
my_set = {1, 2, 3, 4, 5}
my_list = list(my_set)
print(my_list[0]) # Access the first element
4. Using .pop() :
The .pop() method removes and returns an arbitrary element from the set (since sets
are unordered, there's no guarantee about which element will be removed).
my_set = {1, 2, 3}
element = my_set.pop()
print(element) # Prints an arbitrary element (e.g., 1)
print(my_set) # The set after the element is removed
Differences from Lists and Tuples
Uniqueness: Sets automatically eliminate duplicate elements, while lists and tuples can
have duplicates.
my_list = [1, 2, 2, 3]
my_set = {1, 2, 2, 3}
print(my_list) # Output: [1, 2, 2, 3]
print(my_set) # Output: {1, 2, 3} (no duplicates)
Unordered: Sets are unordered, meaning they don’t maintain the insertion order of
elements like lists and tuples do. The order of elements in a set can change.
my_set = {3, 1, 4, 2}
print(my_set) # Output: {1, 2, 3, 4} (order may vary)
Mutable: Like lists, you can add or remove elements from a set. However, the elements
themselves must be immutable (i.e., you cannot store lists or other sets as elements).
my_set.add(6) # Add an element to the set
print(my_set) # Output: {1, 2, 3, 4, 6}
my_set.remove(1) # Remove an element from the set
print(my_set) # Output: {2, 3, 4, 6}
No indexing or slicing: Since sets are unordered, you cannot access elements by index,
unlike lists and tuples.
my_set = {1, 2, 3}
# my_set[0] # This will raise a TypeError
Special Methods for Sets
.add(element) : Adds an element to the set.
my_set = {1, 2, 3}
my_set.add(4)
print(my_set) # Output: {1, 2, 3, 4}
.remove(element) : Removes a specific element from the set. Raises a KeyError if the
element is not found.
my_set.remove(2)
print(my_set) # Output: {1, 3, 4}
.discard(element) : Removes an element if it exists, but does not raise an error if the
element is missing.
my_set.discard(5) # No error even though 5 is not in the set
.pop() : Removes and returns an arbitrary element from the set. Raises KeyError if the
set is empty.
element = my_set.pop()
print(element) # Output: an arbitrary element (since sets are unordered)
.clear() : Removes all elements from the set.
my_set.clear()
print(my_set) # Output: set()
.union(other_set) : Returns a new set that is the union of two sets (all elements in either
set).
set1 = {1, 2, 3}
set2 = {3, 4, 5}
union_set = set1.union(set2)
print(union_set) # Output: {1, 2, 3, 4, 5}
.intersection(other_set) : Returns a new set with elements common to both sets.
intersect_set = set1.intersection(set2)
print(intersect_set) # Output: {3}
.difference(other_set) : Returns a new set with elements that are in the first set but not
in the second.
diff_set = set1.difference(set2)
print(diff_set) # Output: {1, 2}
.issubset(other_set) : Checks if the set is a subset of another set.
print(set1.issubset({1, 2, 3, 4})) # Output: True
.issuperset(other_set) : Checks if the set is a superset of another set.
print(set1.issuperset({1, 2})) # Output: True
Use cases
Sets are particularly useful when you need to work with unique items or perform set
operations such as unions, intersections, and differences. Below are some common use cases
where sets are ideal and demonstrate why they should be used instead of lists or tuples:
1. Removing Duplicate Elements
You have a collection with duplicate items and need to remove the duplicates efficiently.
data = [1, 2, 2, 3, 4, 4, 5]
unique_data = set(data)
print(unique_data) # Output: {1, 2, 3, 4, 5}
2. Fast Membership Testing
You need to frequently check whether an element exists in a collection.
Checking for membership ( in operation) in a set is much faster (average time complexity
is O(1)) compared to lists or tuples (O(n)).
my_set = {1, 2, 3, 4, 5}
print(3 in my_set) # Output: True (fast lookup)
3. Set Operations (Union, Intersection, Difference)
You need to perform mathematical set operations, such as finding the union, intersection,
or difference between two collections.
set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}
# Union (all unique elements from both sets)
print(set1.union(set2)) # Output: {1, 2, 3, 4, 5, 6}
# Intersection (common elements in both sets)
print(set1.intersection(set2)) # Output: {3, 4}
# Difference (elements in set1 but not in set2)
print(set1.difference(set2)) # Output: {1, 2}
4. Filtering Unique Items Across Multiple Lists
Use case: You want to compare and filter unique items across different collections,
such as removing overlapping elements.
Why use a set?: Sets inherently handle uniqueness and allow you to compare items
across collections.
list1 = [1, 2, 3, 4]
list2 = [3, 4, 5, 6]
# Convert lists to sets and find the unique elements in both
unique_elements = set(list1) | set(list2) # Union of the sets
print(unique_elements) # Output: {1, 2, 3, 4, 5, 6}
5. Handling Large Datasets with Unique Elements
Use case: You are working with a large dataset where you need to ensure that
elements are unique and operations like union or intersection should be efficient.
Why use a set?: Sets provide better performance with large datasets, especially
when uniqueness or set operations are important. Lists and tuples, on the other hand,
are slower for these operations.
large_set1 = set(range(1000000))
large_set2 = set(range(500000, 1500000))
common_elements = large_set1 & large_set2 # Intersection
print(len(common_elements)) # Output: 500000
6. Finding Missing or Extra Elements in Two Lists
You need to quickly find out which elements are present in one list but missing in another,
or vice versa.
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7]
set1 = set(list1)
set2 = set(list2)
missing_in_list2 = set1.difference(set2)
print(missing_in_list2) # Output: {1, 2, 3}
extra_in_list2 = set2.difference(set1)
print(extra_in_list2) # Output: {6, 7}
processed_data = set() # Keep track of processed items
for item in data_stream:
if item not in processed_data:
process(item)
processed_data.add(item) # Mark as processed
7. Tracking Unique Elements (e.g., Unique Words in Text)
You want to track the unique words in a document or text while ignoring duplicates.
text = "hello world hello everyone"
words = set(text.split()) # Split text into words and remove duplicates
print(words) # Output: {'world', 'hello', 'everyone'}
8. Graph Algorithms
In graph-related problems (e.g., finding connected components), sets are used to track
visited nodes because they ensure uniqueness and provide fast membership checking.
visited = set()
def dfs(node):
if node not in visited:
visited.add(node)
# Visit neighbors
When not to use a set:
Order matters: If you need to maintain the insertion order of elements (like in lists or
tuples), sets are not suitable.
Duplicate elements needed: If duplicates are important, sets will remove them, so a list
or tuple would be a better choice.
Indexing is needed: Since sets are unordered, you cannot access elements via index, so
if indexing is required, a list or tuple is better.
Dictionaries
A dictionary in Python is an unordered collection of key-value pairs where each key must be
unique and immutable, but values can be any data type and even mutable (like lists).
1. Creation
Creating an empty dictionary:
my_dict = {}
# or
my_dict = dict()
Creating a dictionary with initial values:
my_dict = {
"name": "Alice",
"age": 25,
"city": "New York"
}
Creating a dictionary from a list of tuples:
list_of_tuples = [("name", "Alice"), ("age", 25)]
my_dict = dict(list_of_tuples)
Creating a dictionary using dictionary comprehension:
squares = {x: x**2 for x in range(1, 6)}
print(squares) # Output: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
2. Accessing Elements
Accessing values using keys:
my_dict = {"name": "Alice", "age": 25, "city": "New York"}
# Accessing using a key
print(my_dict["name"]) # Output: Alice
# Using .get() method (returns None if key is not found)
print(my_dict.get("age")) # Output: 25
print(my_dict.get("job", "Not Found")) # Output: Not Found
Modifying values:
my_dict["age"] = 26
print(my_dict) # Output: {'name': 'Alice', 'age': 26, 'city': 'New York'}
Adding new key-value pairs:
my_dict["job"] = "Engineer"
print(my_dict) # Output: {'name': 'Alice', 'age': 26, 'city': 'New York', 'job':
'Engineer'}
Removing elements:
my_dict.pop("age") # Removes 'age' key
del my_dict["city"] # Removes 'city' key
3. Difference from Lists, Tuples, and Sets
Key Differences:
Dictionaries vs. Lists:
Lists store ordered elements accessed by their index, while dictionaries store key-
value pairs accessed by keys.
Lists allow duplicate elements, but dictionary keys must be unique.
Dictionaries vs. Tuples:
Tuples are immutable and ordered, meaning their content cannot change and
elements are accessed by index. Dictionaries, on the other hand, are mutable,
unordered collections accessed by key.
Dictionaries vs. Sets:
Sets are collections of unique, unordered elements, but they don't have associated
values. Dictionaries also require unique keys but store key-value pairs.
Summary of Differences:
Collection Ordered Mutable Allows Keyed Ideal Use Case
Duplicates Access
List Yes Yes Yes No Sequential data
storage
Tuple Yes No Yes No Immutable
sequences
Set No Yes No No Unique elements
Dictionary No Yes No (for keys) Yes Key-value mappings
4. Special Methods
Dictionaries come with several special methods for performing common operations efficiently:
.keys() : Returns a view object of all the keys in the dictionary.
my_dict.keys() # Output: dict_keys(['name', 'age'])
.values() : Returns a view object of all the values in the dictionary.
my_dict.values() # Output: dict_values(['Alice', 25])
.items() : Returns a view object of key-value pairs (as tuples).
my_dict.items() # Output: dict_items([('name', 'Alice'), ('age', 25)])
.update() : Updates the dictionary with elements from another dictionary or key-value
pairs.
my_dict.update({"age": 26, "city": "New York"})
.pop() : Removes the item with the specified key and returns its value.
my_dict.pop("name") # Removes 'name' and returns 'Alice'
.clear() : Removes all items from the dictionary.
my_dict.clear() # Empties the dictionary
5. Use Cases of Dictionaries
1. Mapping Data:
Ideal for mapping key-value pairs, such as:
Storing user data ( {"name": "Alice", "age": 25} ).
Phone books (mapping names to phone numbers).
Configuration settings (mapping keys to system settings).
2. Counting Elements (Histograms):
Counting occurrences of items in a list:
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_count = {}
for word in words:
word_count[word] = word_count.get(word, 0) + 1
print(word_count) # Output: {'apple': 3, 'banana': 2, 'orange': 1}
3. Storing Complex Data:
Nested dictionaries can store structured data.
students = {
"Alice": {"age": 25, "grade": "A"},
"Bob": {"age": 24, "grade": "B"}
}
print(students["Alice"]["grade"]) # Output: A
4. Lookups with Fast Access:
In scenarios where you need fast lookups based on a specific attribute (like a unique ID or
key), dictionaries offer O(1) average time complexity for key-based access.
employee_salaries = {"Alice": 70000, "Bob": 65000}
print(employee_salaries["Alice"]) # Output: 70000
6. When Not to Use Dictionaries
Order Matters: If ordering matters, lists or tuples may be more appropriate.
Duplicates Needed: If you need to store duplicate items, dictionaries are not suitable
since dictionary keys must be unique.
Index-based Access Needed: Unlike lists or tuples, you can't access elements by index
in a dictionary, so use a list if you need to perform index-based operations.
Memory Efficiency: Dictionaries can use more memory compared to lists or tuples
because of the overhead required to store keys and values, so for large datasets with
simple sequences, lists or sets might be more memory-efficient.
Conclusion
Dictionaries are perfect for storing key-value pairs where fast lookups, updates, and unique
keys are important. They are not suitable when ordering or duplicates are required, or when
index-based access is needed. Use dictionaries to solve problems that require fast lookups,
mapping data, or complex structures, and avoid them when your use case benefits from
ordered or sequential data.