Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Module 3a: Python Basics for Water Modellers

Your First Calculations in Python

Time required: 20-25 minutes
Prerequisites: Module 2b (uv installed, Python environment ready)
What you’ll learn:

  • Understand notebook cells (markdown vs. code)

  • Work with variables and basic calculations

  • Use imports to access additional functionality

  • Create your first hydraulic function


Why this matters: Before we dive into real streamflow data analysis, you need to understand how Python notebooks work and the fundamental building blocks of Python code. This module gives you those foundations through practical hydraulic engineering examples.

Coming from Excel? You’re Already Halfway There!

If you’ve been using Excel for water modelling calculations, you already understand the core concepts—you just need to learn new syntax. Here’s the good news: everything you do in Excel has a Python equivalent, and Python makes it easier to repeat, share, and scale your work.

Excel → Python Translation

What You Do in ExcelPython Equivalent
Cell with a number: 15.5Variable: Q = 15.5
Formula: =A1 + B1Expression: a + b
=SUM(A:A)df['A'].sum()
=AVERAGE(A:A)df['A'].mean()
=MAX(A:A)df['A'].max()
Filter rows by valuedf[df['Q'] > 10]
VLOOKUPdf.merge() or df.join()
Pivot Tabledf.groupby().agg()
Chartmatplotlib plotting

Don’t worry if the Python column looks unfamiliar—by the end of Module 4, you’ll understand all of these!

Why Make the Switch?

Excel LimitationPython Solution
Manual repetition (copy formulas for each station)Write once, apply to 100 stations automatically
“Which version is correct?” (v2_final_FINAL.xlsx)Version control tracks every change
Hard to share methodsCode documents exactly what you did
Breaks with large datasetsHandles millions of rows efficiently
Formula errors hide in cellsErrors are explicit and traceable

The Mindset Shift

In Excel: You work interactively, clicking and dragging, seeing results immediately.

In Python: You write instructions (code) that the computer follows. This feels slower at first, but:

  • You can re-run the same analysis on new data instantly

  • Your colleague can see exactly what you did

  • You can automate weekly/monthly reports

  • Results are reproducible years later

💡 Think of Python code as a recipe: Excel is like cooking by taste—you know it works but can’t easily explain how. Python is like a detailed recipe—anyone can follow it and get the same result.


Now let’s learn the Python building blocks that make this possible!

Understanding Jupyter Notebook Cells

A Jupyter notebook is made up of cells. There are two main types you’ll use:

1. Markdown Cells (Text)

These cells contain formatted text, like this one you’re reading now. They’re perfect for:

  • Explanations and documentation

  • Headings and structure

  • Equations written in LaTeX

  • Images and links

This cell is a markdown cell. Notice it displays formatted text, not code.

2. Code Cells (Python)

These cells contain executable Python code. When you run them, Python executes the code and shows any results below the cell.

First time opening a notebook?
When you first open a notebook in VS Code, you’ll see a ▶️ Run button next to code cells and not yet this [ ]. Don’t worry - this is normal! The first time you try to run a code cell, VS Code will ask you to select a Python interpreter.

Selecting the Python Interpreter

When you click the Run button for the first time, you’ll see a dropdown menu asking you to “Select Kernel” or “Select Python Interpreter”. Here’s what to do:

  1. Look for your project’s uv environment - It should be listed as something like:

    • Python 3.x.x ('.venv': venv)

    • Or it might show the full path: .venv/bin/python

  2. If you don’t see it, click “Python Environments...” and select:

    • The .venv folder in your project directory

    • Or run uv sync in the terminal first to create the environment

  3. After selecting, the notebook will connect to your Python environment and you’ll see [ ]: appear next to code cells

💡 Why does this matter? Selecting the right interpreter ensures your notebook uses the Python packages you installed with uv (like pandas, matplotlib, etc.) in your project’s virtual environment.

What You’ll See

Once connected to the Python interpreter:

  • Code cells show [ ]: on the left before you run them

  • After running, they show [1]: (or another number) indicating execution order

  • They may display output below

Try It: Your First Code Cell

The cell below is a code cell. Let’s run it!

To run a code cell:

  • Click on the cell to select it

  • Press Shift + Enter (or click the ▶️ play button on the left)

# This is a code cell!
# Lines starting with # are comments - Python ignores them
# They're for humans to read

print("Hello, Water Modeller!")

What happened?

  • The print() function displayed text

  • The # symbol marks comments (notes for humans, ignored by Python)

  • The cell executed and showed output below

Important: Run cells in order! Notebooks are meant to be run from top to bottom. Later cells often depend on variables or imports from earlier cells. If you skip a cell or run them out of order, you may get errors like NameError: name 'variable' is not defined.

If things stop working, try: Kernel → Restart and then run all cells from the beginning.

Variables: Storing Values

In hydraulic calculations, we work with many values: discharge, velocity, area, slope. Variables let us store these values and reuse them.

Think of a variable as a labeled container:

Q = 15.5    # Discharge in m³/s

Here, Q is the variable name, and 15.5 is the value stored inside it.

Example: Manning’s Formula Calculation

Let’s calculate flow velocity using a simplified version of Manning’s equation. First, we’ll store our input values in variables:

# Channel properties
hydraulic_radius = 2.5  # meters
slope = 0.001           # m/m (0.1% slope)
mannings_n = 0.035      # roughness coefficient for natural channel

print("Hydraulic radius:", hydraulic_radius, "m")
print("Channel slope:", slope, "m/m")
print("Manning's n:", mannings_n)

Notice:

  • Variable names describe what they contain (hydraulic_radius, not just R)

  • We can use underscores to make names readable

  • Numbers with decimals use a period (.) not a comma

  • Comments explain the units (crucial in engineering!)

Performing Calculations

Now we can use these variables in calculations. Python uses standard math operators:

OperationOperatorExample
Addition+a + b
Subtraction-a - b
Multiplication*a * b
Division/a / b
Exponentiation**a ** 2 (a squared)

Let’s calculate velocity using Manning’s formula: v=1nR2/3S1/2v = \frac{1}{n} R^{2/3} S^{1/2}

# Manning's equation for velocity
velocity = (1 / mannings_n) * (hydraulic_radius ** (2/3)) * (slope ** 0.5)

print("Flow velocity:", velocity, "m/s")

What happened?

  • Python calculated the result using our variables

  • The ** operator raises a number to a power (e.g., hydraulic_radius ** (2/3))

  • We stored the result in a new variable called velocity

  • print() displayed the result

💡 Key insight: Variables let us change inputs and recalculate easily. Try changing slope above and re-running both cells!

Imports: Using Additional Tools

Python comes with basic math operations, but for engineering work we need more powerful tools. That’s where imports come in.

An import statement loads additional functionality from Python libraries (also called packages or modules).

Think of it like opening a toolbox:

import math  # Opens the 'math' toolbox

Now we can use tools from that toolbox:

math.sqrt(25)  # Use the square root tool: √25 = 5

Example: Calculating Pipe Flow

Let’s calculate flow rate in a circular pipe using the continuity equation (Q = A × v). We’ll need the math module for π and square root:

# Import the math module
import math

# Pipe properties
diameter = 0.5      # meters
velocity = 1.2      # m/s

# Calculate cross-sectional area (A = π r²)
radius = diameter / 2
area = math.pi * radius ** 2

# Calculate discharge (Q = A × v)
discharge = area * velocity

print(f"Pipe diameter: {diameter} m")
print(f"Flow velocity: {velocity} m/s")
print(f"Cross-sectional area: {area:.4f} m²")
print(f"Discharge: {discharge:.4f} m³/s")

New concepts here:

  1. import math: Loads the math module

  2. math.pi: Uses the constant π from the math module (≈3.14159)

  3. f"...{variable}...": Format strings (f-strings) that insert variable values into text

  4. {area:.4f}: Formats numbers to 4 decimal places

Common imports you’ll use in water modeling:

  • import math - Mathematical functions and constants

  • import numpy - Numerical computing (arrays, advanced math)

  • import pandas - Working with data tables

  • import matplotlib.pyplot - Creating plots and graphs

Functions: Packaging Reusable Calculations

Imagine you need to calculate discharge for 50 different pipes. Copying the same calculation code 50 times would be tedious and error-prone.

Functions solve this problem. A function is a reusable block of code that:

  • Takes inputs (called parameters or arguments)

  • Performs calculations

  • Returns results

Anatomy of a Function

def function_name(parameter1, parameter2):
    """This is a docstring - it explains what the function does"""
    # calculations go here
    result = parameter1 + parameter2
    return result
  • def: Keyword that starts a function definition

  • function_name: Choose a descriptive name

  • (parameter1, parameter2): Inputs the function needs

  • """ docstring """: Documentation explaining the function

  • return: Sends the result back to whoever called the function

Example: Discharge Calculation Function

Let’s create a function to calculate discharge in a circular pipe:

import math

def calculate_pipe_discharge(diameter, velocity):
    """
    Calculate discharge in a circular pipe.
    
    Parameters:
    -----------
    diameter : float
        Pipe diameter in meters
    velocity : float
        Average flow velocity in m/s
    
    Returns:
    --------
    float
        Discharge in m³/s
    """
    # Calculate cross-sectional area
    radius = diameter / 2
    area = math.pi * radius ** 2
    
    # Calculate discharge
    discharge = area * velocity
    
    return discharge

Now we can use this function for any pipe! Let’s calculate discharge for several different scenarios:

# Small pipe, low velocity
Q1 = calculate_pipe_discharge(diameter=0.3, velocity=0.8)
print(f"Scenario 1: Q = {Q1:.4f} m³/s")

# Medium pipe, moderate velocity
Q2 = calculate_pipe_discharge(diameter=0.6, velocity=1.5)
print(f"Scenario 2: Q = {Q2:.4f} m³/s")

# Large pipe, high velocity
Q3 = calculate_pipe_discharge(diameter=1.2, velocity=2.0)
print(f"Scenario 3: Q = {Q3:.4f} m³/s")

Benefits of functions:

  1. Reusability: Write once, use many times

  2. Clarity: The function name describes what it does

  3. Maintenance: Fix a bug in one place, not everywhere

  4. Documentation: The docstring explains inputs, outputs, and units

💡 Best practice for engineering: Always document units in your docstrings!

Example: Critical Depth in a Rectangular Channel

Let’s create a function to calculate critical depth in a rectangular channel.

The formula is: yc=(q2g)1/3y_c = \left(\frac{q^2}{g}\right)^{1/3}

Where:

  • ycy_c = critical depth (m)

  • qq = discharge per unit width (m²/s)

  • gg = gravitational acceleration (9.81 m/s²)

import math

def calculate_critical_depth(discharge_per_width):
    """
    Calculate critical depth in a rectangular channel.
    
    Parameters:
    -----------
    discharge_per_width : float
        Discharge per unit width (q) in m²/s
    
    Returns:
    --------
    float
        Critical depth in meters
    """
    g = 9.81  # gravitational acceleration in m/s²
    
    # Calculate critical depth: yc = (q²/g)^(1/3)
    critical_depth = (discharge_per_width ** 2 / g) ** (1/3)
    
    return critical_depth

# Test the function
q = 2.5  # m²/s
yc = calculate_critical_depth(q)
print(f"For q = {q} m²/s, critical depth = {yc:.3f} m")

Result: For q = 2.5 m²/s, the critical depth is approximately 0.860 m.

💡 Try it yourself: Change the value of q in the cell above and re-run it to see how critical depth changes with different discharge rates!

Summary: What You’ve Learned

Congratulations! You now understand the fundamental building blocks of Python programming:

1. Notebook Cells

  • Markdown cells: Formatted text, documentation, equations

  • Code cells: Executable Python code

  • Run cells with Shift + Enter

2. Variables

discharge = 15.5  # Store values in named containers

3. Calculations

velocity = discharge / area  # Use standard operators: +, -, *, /, **

4. Imports

import math  # Load additional functionality
value = math.sqrt(25)  # Use functions from imported modules

5. Functions

def calculate_something(input1, input2):
    """Documentation here"""
    result = input1 + input2
    return result

Key Principles for Engineering Code:

  • ✅ Use descriptive variable names

  • ✅ Always document units in comments and docstrings

  • ✅ Write reusable functions for repeated calculations

  • ✅ Test your code with known values

What’s Next?

Now that you understand these Python basics, you’re ready to:

Module 3b: AI-Assisted Coding

  • Learn to use AI assistants (ChatGPT, Claude) effectively for coding

  • Write better prompts to get working code

  • Understand AI limitations for engineering work

You’ll see these concepts everywhere:

  • Variables storing discharge timeseries

  • Imports bringing in pandas and matplotlib

  • Functions processing hydrological data

The complexity increases, but the fundamentals stay the same!

Optional Challenge: Froude Number

If you want more practice, try creating a function to calculate the Froude number:

Fr=vgyFr = \frac{v}{\sqrt{g \cdot y}}

Where:

  • FrFr = Froude number (dimensionless)

  • vv = flow velocity (m/s)

  • gg = 9.81 m/s²

  • yy = flow depth (m)

Bonus: Add logic to determine if the flow is subcritical (Fr < 1), critical (Fr = 1), or supercritical (Fr > 1).

# Your solution here:

import math

def calculate_froude_number(velocity, depth):
    """
    Calculate Froude number for open channel flow.
    
    Parameters:
    -----------
    velocity : float
        Flow velocity in m/s
    depth : float
        Flow depth in m
    
    Returns:
    --------
    float
        Froude number (dimensionless)
    """
    g = 9.81  # m/s²
    
    # TODO: Calculate Froude number using the formula: Fr = v / sqrt(g * y)
    # Your code here
    
    return 0.0  # Placeholder - replace with your calculation

# Test your function:
# Fr = calculate_froude_number(velocity=2.0, depth=1.5)
# print(f"Froude number: {Fr:.3f}")

# BONUS: Add if/elif/else logic to determine flow regime
# if Fr < 1:
#     print("Flow regime: Subcritical")
# elif Fr > 1:
#     print("Flow regime: Supercritical")
# else:
#     print("Flow regime: Critical")
Click to reveal solution
import math

def calculate_froude_number(velocity, depth):
    """
    Calculate Froude number for open channel flow.
    
    Parameters:
    -----------
    velocity : float
        Flow velocity in m/s
    depth : float
        Flow depth in m
    
    Returns:
    --------
    tuple
        (Froude number, flow regime string)
    """
    g = 9.81  # m/s²
    
    # Calculate Froude number: Fr = v / sqrt(g * y)
    froude = velocity / math.sqrt(g * depth)
    
    # Determine flow regime
    if froude < 1:
        regime = "Subcritical"
    elif froude > 1:
        regime = "Supercritical"
    else:
        regime = "Critical"
    
    return froude, regime

# Test the function
Fr, regime = calculate_froude_number(velocity=2.0, depth=1.5)
print(f"Froude number: {Fr:.3f}")
print(f"Flow regime: {regime}")

# Test with subcritical flow
Fr2, regime2 = calculate_froude_number(velocity=0.5, depth=2.0)
print(f"\nSubcritical test: Fr = {Fr2:.3f} ({regime2})")

# Test with supercritical flow
Fr3, regime3 = calculate_froude_number(velocity=5.0, depth=0.3)
print(f"Supercritical test: Fr = {Fr3:.3f} ({regime3})")

Expected output:

Froude number: 0.521
Flow regime: Subcritical

Subcritical test: Fr = 0.113 (Subcritical)
Supercritical test: Fr = 2.914 (Supercritical)

Next Module: Module 3b: AI-Assisted Coding

Learn how to use AI assistants to help you write Python code more effectively!