Python 101

Python is a versatile language, widely used for urban analysis and data science. This guide introduces its foundational concepts.

The best way to learn Python is by trying things out. Change the examples, break them, and see what happens. Progress comes fastest when you actively use the tools to solve problems and learn as you go.

This is a brief introduction to Python. To use it effectively day-to-day, you’ll need hands-on practice and to explore further resources, which are widely available online. It’s a great time to learn, as Large Language Models (LLMs) can help speed up the process and answer many of your questions. However, be mindful: use LLMs to amplify your understanding and guide your learning, not as a substitute for grasping the concepts yourself. Relying on them to do the work without understanding means you might not learn effectively and could miss out on using these tools to their full potential.

Comments

Use # to add comments, which Python ignores. Comments help explain your code.

# This is a comment
print("Hello, World!")  # Print a greeting
Hello, World!

Multiline comments use triple quotes:

"""
This is a multiline comment.
It can span multiple lines.
"""

Variables

Variables act as labelled containers for data. Assign values using =.

# Assigning values to variables
message = "Hello, Python!"  # Text (string)
count = 10                  # Whole number (integer)
price = 19.99               # Decimal number (float)
is_active = False           # True/False value (boolean)

# Accessing variable values
print(message)
print(count)
print(price)
print(is_active)
Hello, Python!
10
19.99
False

Data Types

Variables can hold different types of data, such as numbers (integers or floats) and text (strings). The functionality available often depends on the data type, though some behaviours are shared across types.

  • str (String): Text, enclosed in quotes (" or '). Example: "Urbanism".
  • int (Integer): Whole numbers. Example: 42, -7.
  • float (Float): Numbers with decimals. Example: 3.14, -0.001.
  • bool (Boolean): Logical values, True or False (capitalised).

Check a variable’s type using type():

print(type(message))   # <class 'str'>
print(type(count))     # <class 'int'>
print(type(price))     # <class 'float'>
print(type(is_active)) # <class 'bool'>
<class 'str'>
<class 'int'>
<class 'float'>
<class 'bool'>

When working with variables, especially in Python, be cognisant of the type assigned to the variable. Misusing types can lead to issues and errors.

# This will raise an error because you cannot add a string to an integer
# TypeError: unsupported operand type(s) for +: 'int' and 'str'

count + message

Arithmetic

Python supports standard arithmetic operations:

# Arithmetic operators: +, -, *, /, **, %
a = 10
b = 3

print(a + b)  # Addition: 13
print(a - b)  # Subtraction: 7
print(a * b)  # Multiplication: 30
print(a / b)  # Division (float result): 3.333...
print(a ** b) # Exponentiation: 1000
print(a % b)  # Modulus (remainder): 1
13
7
30
3.3333333333333335
1000
1

Python follows the standard order of operations (often remembered by acronyms like BODMAS or PEMDAS). For example:

result = 2 + 3 * 4 - 5 / 2
print(result)
11.5

If you’re ever unsure about the order, it’s good practice to use parentheses () to make your intention explicit.

result = (2 + 3) * 4 - 5 / 2
print(result)
17.5
result = 2 + 3 * (4 - 5) / 2
print(result)
0.5

Strings

Strings are very versatile and have a number of built-in methods for common use cases.

first_name = "Ada"
last_name = "Lovelace"
full_name = first_name + " " + last_name
print(full_name)  # Output: Ada Lovelace
Ada Lovelace
Note

A method is like a function that’s associated with a particular object or type (like a string). For example, str.upper() is a method of the str class that converts a string to uppercase. You access methods using dot notation (object.method()) and execute them by using parentheses. If a method requires options or parameters, they go inside the parentheses.

Common built-in methods include converting text to lower or upper case, or removing leading/trailing whitespace and characters.

# String methods
print(full_name.lower())  # Lowercase: "ada lovelace"
print(full_name.upper())  # Uppercase: "ADA LOVELACE"
print(full_name.replace(" ", "_"))  # Replace spaces with underscores
ada lovelace
ADA LOVELACE
Ada_Lovelace

F-Strings

F-strings (formatted string literals) offer a convenient way to embed expressions inside string literals for formatting. Simply prefix the string with an f and write expressions in {}.

city = "Berlin"
population = 3800000
info = f"The city of {city} has a population of {population}."
print(info)
The city of Berlin has a population of 3800000.

Exercises

  • Create a variable for a number. Print its type using type().
  • Create a variable for your name, then print a statement using an F-string to say “Hello, [your name]!”.
  • Perform all basic arithmetic operations (+, -, *, /, %) on two numbers and print the results. Experiment with parentheses () to change the order of operations.

Collections

Often, you need to work with multiple pieces of data at once. Python provides several ways to group data, each with its own characteristics.

Type Ordered Unique Mutable Example Syntax Example Use Case
List Yes No Yes [1, 2, 3] Shopping list, coordinates
Tuple Yes No No (1, 2, 3) Fixed coordinates, RGB colour
Set No Yes (Values) Yes {1, 2, 3} Unique tags, deduplication
Dictionary Yes Yes (Keys) Yes {"a": 1, "b": 2} Lookup tables, data records

Lists

Use lists when the order of items is important and you might need to change them later. Lists are mutable (meaning they can be changed after creation). Think of them like a row of mailboxes: you can change what’s inside a mailbox. Access items using their index (position, starting from 0) in square brackets []. Negative indexing is also handy, where -1 refers to the last item, -2 to the second last, and so on.

Create a list using square brackets []:

planets = ["Mercury", "Venus", "Earth", "Mars"]

planets
['Mercury', 'Venus', 'Earth', 'Mars']

Access items by index (position, starting from 0)

print(planets[0]) # Mercury
print(planets[2]) # Earth
Mercury
Earth

Reverse index

print(planets[-1]) # Mars
print(planets[-3]) # Venus
Mars
Venus

Add an item to the end

planets.append("Jupiter")

planets
['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter']
Note

Python has several useful built-in functions. One is len(), which we’ll use below. It returns the length (i.e., the number of items) of an object like a list or a string.

Get the number of items

len(planets) # Output: 5
5

Lists are mutable (can be changed after creation)

planets[0] = "Fast Planet" # Change the first item
planets[3] = "Red Planet" # Update Mars

planets
['Fast Planet', 'Venus', 'Earth', 'Red Planet', 'Jupiter']

Common list operations

planets.insert(1, "New Planet") # Insert at a specific index
print(planets)
['Fast Planet', 'New Planet', 'Venus', 'Earth', 'Red Planet', 'Jupiter']

Remove item at index 2 and get its value

removed_planet = planets.pop(2)
print(f"Removed: {removed_planet}")
print(planets)
Removed: Venus
['Fast Planet', 'New Planet', 'Earth', 'Red Planet', 'Jupiter']

Dictionaries

Use dictionaries when you need to associate values with unique keys (like looking up a word in a dictionary to find its definition). They are very efficient for retrieving values when you know the key.

Once data is stored, you can easily retrieve or update a value using its key.

Dictionaries are defined using curly braces {}. Each item is a key-value pair, separated by a colon :. The key is on the left, and the value is on the right.

Similar to lists, dictionaries use square brackets [] to access values, but instead of a numerical index, you use the key.

# Create a dictionary using curly braces {}
# Format: {key1: value1, key2: value2}
building_info = {
    "type": "Residential",
    "floors": 5,
    "year_built": 1998
}
building_info
{'type': 'Residential', 'floors': 5, 'year_built': 1998}
# Access values using keys
print(building_info["type"]) # Residential
print(building_info["floors"]) # 5
Residential
5
# Add a new key-value pair
building_info["has_elevator"] = True
print(building_info)
{'type': 'Residential', 'floors': 5, 'year_built': 1998, 'has_elevator': True}
# Dictionaries are mutable
building_info["year_built"] = 2000 # Update a value
print(building_info)
{'type': 'Residential', 'floors': 5, 'year_built': 2000, 'has_elevator': True}
# Common dictionary operations
print(building_info.keys())   # Get all keys
print(building_info.values()) # Get all values
print(building_info.get("floors")) # Get value
print(building_info.get("address"))  # Returns None
dict_keys(['type', 'floors', 'year_built', 'has_elevator'])
dict_values(['Residential', 5, 2000, True])
5
None

Tuples

Use tuples for ordered collections of items that shouldn’t change after creation (e.g., coordinates). Tuples are immutable, meaning they cannot be altered once created.

# A tuple of coordinates
point = (10.5, 25.3)
print(point)
print(point[0]) # Access items like lists
(10.5, 25.3)
10.5
# This would cause a TypeError! Tuples are immutable.
point[0] = 11.0

Tuples are often used to return multiple values from functions or for unpacking (we will explain functions later).

def get_coordinates():
    return (10.5, 25.3)

This is a common pattern in Python, especially when dealing with functions that return multiple values. You can unpack the tuple into separate variables.

coords = get_coordinates()
lat, lon = coords # Tuple unpacking
print(f"Latitude: {lat}, Longitude: {lon}")
Latitude: 10.5, Longitude: 25.3

The above can also be done directly:

lat, lon = get_coordinates()
print(f"Latitude: {lat}, Longitude: {lon}")
Latitude: 10.5, Longitude: 25.3

The below would raise an error because the tuple only has two items, but we are trying to unpack it into three variables:

lat, lon, z = get_coordinates()

Sets

Use sets when you need a collection of unique items, and the order of those items isn’t important.

 # Duplicate "London" is ignored
unique_cities = {"London", "Paris", "Berlin", "London"}
# Output might be in any order, e.g., {'Berlin', 'London', 'Paris'}
print(unique_cities)
{'London', 'Paris', 'Berlin'}
# Check for membership
print("Paris" in unique_cities) # True
True
# Add an item
unique_cities.add("Rome")
print(unique_cities)
{'London', 'Paris', 'Berlin', 'Rome'}
# Set operations
set1 = {1, 2, 3}
set2 = {3, 4, 5}
print(set1.union(set2))        # All items from both: {1, 2, 3, 4, 5}
print(set1.intersection(set2)) # Items in both: {3}
print(set1.difference(set2))   # Items in set1 but not set2: {1, 2}
{1, 2, 3, 4, 5}
{3}
{1, 2}

Control Flow

Control flow structures are fundamental to programming. They allow your code to make decisions (using if/elif/else) and repeat actions (using for/while loops).

Conditionals

Conditionals allow your program to execute different blocks of code based on whether certain conditions are True or False. They use comparison operators to evaluate conditions. This conditional logic enables your program to adapt its behaviour to various situations.

# Comparison operators return boolean values
print(5 == 5) # Equal to: True
print(5 != 3) # Not equal to: True
print(5 > 3)  # Greater than: True
print(5 < 3)  # Less than: False
print(5 >= 5) # Greater than or equal to: True
print(5 <= 3) # Less than or equal to: False
True
True
True
False
True
False
Indentation

Indentation is non-negotiable in Python: it defines code blocks. Always use consistent spacing (the common convention is 4 spaces per indentation level) and avoid mixing spaces with tabs. Code blocks are introduced by a colon (:) and can be nested.

# FYI - we will explain loops next
for i in range(3):
    # Example of indentation
    if i == 2:
        # Example of nested indentation
        print(f"{i} is two")
    else:
        print(f"{i} is not two")
0 is not two
1 is not two
2 is two

Use if, elif (short for ‘else if’), and else to direct the flow of execution.

population_density = 5000 # people per sq km

if population_density > 10000:
    print("Very high density")
elif population_density > 3000:
    print("High density")
elif population_density > 1000:
    print("Medium density")
else:
    print("Low density")
High density

Conditions can be combined using and and or.

# Combining conditions with 'and', 'or'
floors = 12

if population_density > 3000 and floors > 10:
    print("High density and tall buildings")
High density and tall buildings

For identity checks (e.g., if a value is None), use is and is not.

# Checking for None (often used for missing data)
maybe_data = None # Represents absence of a value

if maybe_data is None:
    print("Data is missing.")
else:
    print(f"Data found: {maybe_data}")
Data is missing.

For checking if a value is in a collection (like a list or set), use in and not in.

# Check if a value is in a list
cities = ["London", "Paris", "Berlin"]
if "Paris" in cities:
    print("Paris is in the list of cities.")
if "Rome" not in cities:
    print("Rome is not in the list of cities.")
Paris is in the list of cities.
Rome is not in the list of cities.

Loops

Loops are fantastic for avoiding repetitive code. They are essential for processing items in collections and can be combined with conditionals to apply logic selectively to each item.

for Loops

Use for loops to iterate over each item in a sequence (such as a list, tuple, or string). In each pass (iteration), the loop variable (e.g., planet in the example below) takes the value of the current item from the sequence.

# Loop through a list
for planet in planets:
    print(f"Checking planet: {planet}")
Checking planet: Fast Planet
Checking planet: New Planet
Checking planet: Earth
Checking planet: Red Planet
Checking planet: Jupiter

while Loops

Use while loops when you don’t know exactly how many times to loop in advance, but you know the condition under which the loop should continue running.

# Countdown
countdown = 3
while countdown > 0:
    print(countdown)
    # Decrease countdown (essential to avoid infinite loop!!!)
    countdown = countdown - 1
print("Blast off!")
3
2
1
Blast off!

Caution: Ensure the while loop’s condition eventually becomes False, otherwise it will run forever (an infinite loop)!

Exercise

Create a list named numbers containing the integers from 1 to 10. Then, write a loop that iterates through this list. For each number, if it is even, print the number.

Hint: The modulo operator (%) gives the remainder of a division. A number is even if number % 2 == 0.

Functions

Functions help you organise your code into logical, reusable blocks. This improves readability, makes debugging easier, and simplifies maintenance. If you find yourself copying and pasting code, that’s often a good sign you could write a function instead.

# Define a function with parameters (inputs)
def calculate_density(population, area, unit="sq km"):
    """Calculates population density.

    It's good practice to include a *docstring* (documentation string) right after
    the def line to explain what the function does, its parameters, and what it returns.

    Args:
        population (int): The total population.
        area (float): The total area.
        unit (str, optional): The unit for the area. Defaults to "sq km".

    Returns:
        float: The calculated density, or 0 if area is non-positive.
    """
    if area <= 0:
        print("Area must be positive to calculate density.")
        return 0 # Return 0 or raise an error for invalid input
    density = population / area
    return density
  • Parameters: These are the names listed in the function definition’s parentheses (e.g., population, area).
  • Arguments: These are the actual values you pass to the function when you call it.
  • Default Arguments: You can give parameters default values (e.g., unit="sq km"). If you don’t provide an argument for that parameter when calling the function, the default value is used.
  • Scope: Variables defined inside a function are typically local to that function, meaning they only exist and can be accessed within that function.
  • Docstrings: As mentioned, the optional triple-quoted string """...""" right after the def line. They are used to document what the function does, its arguments (often listed under an Args: section), and what it returns (under a Returns: section).
  • Return Statement: The return statement is used to send a value back from the function to the place where it was called. If a function doesn’t have a return statement, or if the return statement is used without a value, it implicitly returns None.

Call the function with arguments (values for the parameters)

density1 = calculate_density(1000000, 50)
print(f"Density 1: {density1:.2f} people per sq km")
Density 1: 20000.00 people per sq km

Calling the function with explicit named arguments allows you to specify which parameter each value corresponds to, making your code clearer and more readable. This is especially useful when a function has many parameters or some have default values.

# Can name arguments
density2 = calculate_density(
    population=500000, area=100, unit="square kilometres"
)
print(f"Density 2: {density2:.2f} people per square kilometres")
Density 2: 5000.00 people per square kilometres

While functions don’t always have to return a value (e.g., a function that just prints something), they often do.

Importing Modules

Python has a vast collection of modules that provide ready-to-use tools – so you often don’t need to reinvent the wheel! If you encounter an unfamiliar function, it’s a good idea to check the import statements, usually found at the top of the file, as the function might come from an imported module.

import module_name makes the functions and variables within that module available, accessed via module_name.function_name.

import math # Import the built-in math module

radius = 5
area = math.pi * (radius ** 2) # Use math.pi
print(f"Circle Area: {area:.2f}")
Circle Area: 78.54
circumference = 2 * math.pi * radius
print(f"Circle Circumference: {circumference:.2f}")
Circle Circumference: 31.42
# You can also import specific methods from a module
from math import sqrt # Import only the square root function
print(f"Square root of 16 is {sqrt(16)}")
Square root of 16 is 4.0

In later sections, we’ll be making extensive use of powerful libraries such as geopandas and numpy, which are imported as modules.

Common Errors

Errors are a normal part of programming! Learning to read and understand error messages is a key skill for fixing them. If you get stuck, searching for the error message online is often helpful – chances are, someone else has encountered the same problem. You can also ask an LLM to help explain what an error message means.

  • NameError: name '...' is not defined: You tried to use a variable before assigning a value to it, or you misspelled the variable name.
  • TypeError: unsupported operand type(s) for ...: You tried to perform an operation on incompatible data types (e.g., adding a string to an integer: "Hello" + 5).
  • SyntaxError: invalid syntax: You made a mistake in the Python grammar (e.g., missing colon :, mismatched parentheses ()).
  • IndexError: list index out of range: You tried to access an item in a list using an index that doesn’t exist (e.g., accessing my_list[5] when the list only has 3 items).
  • ImportError: No module named '...' or ModuleNotFoundError: No module named '...': You tried to import a module that Python can’t find. It might be misspelled or not installed.

Debugging Tip: When you encounter an error, the last line of the message usually tells you the specific type of error. The lines mentioned in the traceback help you find where the problem occurred in your code. Sprinkling print() statements in your code to check the values of variables at different stages can also be a very effective way to debug. And remember, it’s common to fix one error only to find another – just tackle them one by one.