12 Python Fundamentals
In Unit 1, you learned to work with your computer through the command line. In Unit 2, you learned to query and transform data with SQL. Now you’ll learn Python, the language that ties everything together.
This chapter sets up your Python environment, introduces the core building blocks of the language, and highlights the places where Python’s design differs from what you might expect. If you’ve written code in Matlab or another language before, some of this will feel familiar. Pay close attention to the sections on truthiness and short-circuit evaluation. They’re the parts that trip up experienced programmers coming from other languages, and Python leans on these behaviors heavily.
12.1 Why Python?
Every tool you’ve learned so far has a specific job. SQL retrieves and transforms data inside a database. Git tracks changes to files over time. The command line navigates your file system and runs programs. Each tool is powerful within its domain, but none of them can do everything.
Python is different. It’s a general-purpose programming language, which means it wasn’t designed for any single task. Instead, it’s designed to be flexible enough to handle almost anything: reading files, connecting to databases, building web applications, training machine learning models, automating reports, and more. Python isn’t the best tool for any one of these tasks, but it’s good enough at all of them, and it excels at connecting specialized tools together into complete workflows.
Consider a realistic engineering task: every Monday morning, you need to pull last week’s production data from a database, compute quality metrics, flag any lines that fell below threshold, generate a summary report, and email it to your team. SQL can pull the data. Excel can compute the metrics. Outlook can send the email. But who connects those steps? That’s Python’s job. It orchestrates the workflow from start to finish, handling the logic, file management, and automation that no single specialized tool can do alone.
This orchestration role explains why Python has become the common language across data science, data engineering, web development, and scientific computing. It’s not because Python is the fastest language (it isn’t) or the most mathematically elegant (Matlab and R have stronger claims there). It’s because Python is the language that connects everything else.
Matlab excels at numerical computation, matrix operations, and simulation. If your work is primarily solving systems of equations or running Simulink models, Matlab is the right tool. R excels at statistical analysis and publication-quality data visualization. If you’re doing formal hypothesis testing or building statistical models, R has deeper support. SQL excels at querying and transforming data inside databases. You learned this in the previous module, and you’ll continue using it alongside Python.
Python doesn’t replace any of these. It fills the gaps between them and handles the tasks none of them were designed for: file processing, automation, API integration, and workflow orchestration.
12.2 Setting Up Your First Project
In Unit 1, you used uv to manage tools. Now you’ll use it to manage Python itself. Install uv and Python 3.13 following the instructions in Appendix H. Verify the installation:
terminal
uv python list --only-installedYou should see Python 3.13 in the output.
12.2.1 Creating a Project
Every Python project starts with uv init. Navigate to where you want to create your project and run:
terminal
uv init my-project
cd my-projectThis creates a directory with the following structure:
output
my-project/
├── pyproject.toml # project metadata and dependencies
├── .python-version # pins the Python version
├── README.md # project documentation
├── .gitignore # files Git should ignore
└── hello.py # starter script
Each of these files serves a specific purpose. The .python-version file pins the exact Python version so that anyone who clones your project gets the same one. The .gitignore file tells Git to skip files that shouldn’t be tracked, like the virtual environment directory. The README.md is the front door of your project, a topic we’ll revisit in Chapter 22.
The most important file is pyproject.toml. Open it in Zed:
pyproject.toml
[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []This file describes your project: its name, version, which Python version it needs, and what external packages it depends on. For now, it’s mostly empty. In Chapter 22, you’ll learn to use pyproject.toml extensively. For now, just know that it exists and that uv reads it to manage your project.
12.2.2 Running Your First Script
Open hello.py in Zed. You’ll see:
hello.py
def main():
print("Hello from my-project!")
main()Run it from the terminal:
terminal
uv run hello.pyoutput
Hello from my-project!
The uv run command does several things behind the scenes: it creates a virtual environment if one doesn’t exist, installs any dependencies listed in pyproject.toml, and then executes your script using the correct Python version. You don’t need to activate environments manually or worry about which Python is being used. uv run handles all of it.
uv run instead of python?
Running python hello.py directly uses whatever Python happens to be on your system PATH, which might be the wrong version or might not have your project’s dependencies installed. uv run hello.py always uses the right Python with the right packages. Make it a habit: always use uv run for this book.
12.4 The REPL
The REPL (Read-Eval-Print Loop) is Python’s interactive prompt. It reads an expression you type, evaluates it, prints the result, and loops back for more input. Start it from your project directory:
terminal
uv run pythonYou’ll see something like:
output
Python 3.13.2 (main, Feb 4 2025, 14:51:09) [Clang 19.1.6] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
The >>> prompt is where you type Python expressions. Try it:
Python REPL
>>> 2 + 3
5
>>> "hello" + " " + "world"
'hello world'
>>> 7 / 2
3.5
>>> 7 // 2
3The REPL is a scratchpad. Use it when you want to test an expression, check how a function behaves, or explore an unfamiliar library. It’s faster than writing a script, running it, reading the output, and editing the script again.
That said, the REPL isn’t a substitute for scripts. Anything you type in the REPL vanishes when you close it. If your work needs to be saved, versioned with Git, or run again later, write a script. The REPL is for exploration; scripts are for production.
Type exit() or press Ctrl+D to leave the REPL and return to your terminal.
12.5 Variables and Types
A variable is a name attached to a value. In Python, you create a variable by assigning to it with =:
variables.py
product_name = "Widget A"
unit_price = 14.99
quantity = 250
in_stock = TruePython is dynamically typed, which means you don’t declare a variable’s type before using it. Python figures out the type from the value you assign. This is different from languages like C or Java, where you must declare types explicitly, and subtly different from Matlab, where everything is a matrix by default.
12.5.1 Core Types
Python has a small set of built-in types that cover most everyday needs:
| Type | Description | Examples |
|---|---|---|
int |
Whole numbers | 42, -7, 0, 1_000_000 |
float |
Decimal numbers | 3.14, -0.001, 1e6 |
str |
Text (strings) | "hello", 'world', "" |
bool |
Boolean | True, False |
None |
The absence of a value | None |
You can check the type of any value using the type() function:
type_checking.py
type(42) # <class 'int'>
type(3.14) # <class 'float'>
type("hello") # <class 'str'>
type(True) # <class 'bool'>
type(None) # <class 'NoneType'>Python lets you use underscores as visual separators in large numbers: 1_000_000 is the same as 1000000. This is purely for readability and has no effect on the value.
12.5.2 Integers and Floats
Python’s int type has no size limit. You can represent arbitrarily large whole numbers without worrying about overflow:
big_int.py
big_number = 2 ** 100
print(big_number) # 1267650600228229401496703205376Floats, on the other hand, follow the IEEE 754 standard for double-precision floating point. This means they have limited precision, and you’ll occasionally see rounding artifacts:
float_precision.py
0.1 + 0.2 # 0.30000000000000004This isn’t a Python bug. It’s a fundamental property of how computers represent decimal numbers in binary. For most engineering calculations, the precision is more than sufficient. When exact decimal arithmetic matters (financial calculations, for example), Python provides the decimal module.
12.5.3 Strings
Strings are sequences of characters, enclosed in either single quotes or double quotes:
strings.py
name = "Northwind Traders"
greeting = 'Hello, world!'Both forms are identical in behavior. Pick one convention and stick with it. Throughout this book, we’ll use double quotes for strings, matching the convention enforced by the Ruff formatter you’ll meet in Chapter 23.
Strings in Python are immutable: once created, they can’t be changed in place. Operations that appear to modify a string actually create a new one:
string_immutability.py
name = "widget"
upper_name = name.upper() # Creates a new string "WIDGET"
print(name) # Still "widget", unchanged12.5.4 None
None is Python’s way of representing “nothing” or “no value.” If you’ve used SQL, think of None as Python’s equivalent of NULL. It’s not zero, not an empty string, and not False. It’s the explicit absence of a value:
none.py
result = None # We haven't computed the result yetYou’ll use None frequently as a default value, a return value for functions that don’t produce a result, and a signal that something hasn’t been initialized. We’ll revisit None handling extensively in later chapters.
12.5.5 Variable Naming Conventions
Python has a strong convention for naming variables: use snake_case. All letters are lowercase, with words separated by underscores:
naming.py
# Good: snake_case
unit_price = 14.99
order_count = 42
is_discontinued = False
# Bad: other conventions
unitPrice = 14.99 # camelCase (Java/JavaScript convention)
OrderCount = 42 # PascalCase (reserved for class names in Python)
UNIT_PRICE = 14.99 # SCREAMING_SNAKE_CASE (reserved for constants)This isn’t just style. It’s a signal. When you see snake_case, you know it’s a variable or function. When you see PascalCase, you know it’s a class. When you see SCREAMING_SNAKE_CASE, you know it’s a constant. These conventions make Python code readable across teams and projects.
12.5.6 Exercises
Predict the output of
type(True + 1)andtype(1.0 + 2). Then test in the REPL. Why does Python return those types?A variable is assigned
temperature = 72. Write expressions to convert it to Celsius and Kelvin using the formulas C = (F - 32) × 5/9 and K = C + 273.15. Use descriptive variable names.What is the difference between
None,0,"", andFalse? Write a short script that assigns each to a variable and printstype()for each. Are any of them equal to each other when compared with==?Predict what happens when you run
product_name = "Chai"and thenproduct_Name(note the capital N). Why?
1. type(True + 1) → <class 'int'> because True is treated as 1 in arithmetic. type(1.0 + 2) → <class 'float'> because mixing float and int promotes to float.
2.
solution.py
temperature_f = 72
temperature_c = (temperature_f - 32) * 5 / 9
temperature_k = temperature_c + 273.15
print(f"{temperature_f}°F = {temperature_c:.2f}°C = {temperature_k:.2f}K")3. They are all different types (NoneType, int, str, bool). 0 == False is True (bool is a subclass of int), and "" == False is False. None is not equal to any of the others.
4. NameError: name 'product_Name' is not defined. Python variable names are case-sensitive, so product_name and product_Name are completely different names.
12.5.7 Multiple Assignment
Python lets you assign multiple variables in a single statement:
multi_assign.py
x, y, z = 1, 2, 3This also gives you a clean idiom for swapping values:
swap.py
a = 10
b = 20
a, b = b, a # a is now 20, b is now 10In most languages, swapping requires a temporary variable. Python handles it natively because the right side of the assignment is fully evaluated before any assignments happen.
12.6 Operators and Expressions
Python’s arithmetic operators work the way you’d expect, with a few additions that are worth knowing:
arithmetic.py
10 + 3 # 13 Addition
10 - 3 # 7 Subtraction
10 * 3 # 30 Multiplication
10 / 3 # 3.333... Division (always returns float)
10 // 3 # 3 Floor division (rounds down to int)
10 % 3 # 1 Modulo (remainder)
10 ** 3 # 1000 ExponentiationTwo operators deserve special attention. Floor division (//) divides and rounds down to the nearest integer. Modulo (%) returns the remainder after division. Together, they’re useful for tasks like converting units or cycling through indices:
divmod.py
total_minutes = 137
hours = total_minutes // 60 # 2
minutes = total_minutes % 60 # 17
print(f"{hours}h {minutes}m") # 2h 17mIn Python, / always produces a float, even when dividing two integers that divide evenly: 10 / 2 returns 5.0, not 5. If you need an integer result, use //.
12.6.1 String Operations
Strings support concatenation with + and repetition with *:
string_ops.py
first = "North"
last = "wind"
full = first + last # "Northwind"
separator = "-" * 40 # "----------------------------------------"But the most useful string feature in Python is the f-string (formatted string literal). Prefix a string with f and put expressions inside curly braces:
fstrings.py
product = "Widget A"
price = 14.99
quantity = 250
summary = f"{product}: {quantity} units at ${price:.2f} each"
print(summary) # Widget A: 250 units at $14.99 eachThe :.2f inside the braces is a format specification: it formats the number as a float with exactly two decimal places. F-strings can contain any valid Python expression:
fstring_expressions.py
total = f"Total: ${price * quantity:,.2f}"
print(total) # Total: $3,747.50The :,.2f format adds comma separators and two decimal places, perfect for currency.
12.6.2 Comparison Operators
Comparison operators return True or False:
comparisons.py
10 > 5 # True
10 < 5 # False
10 >= 10 # True
10 <= 9 # False
10 == 10 # True (equality check, not assignment)
10 != 5 # True (not equal)== vs. =
A single = is assignment: x = 5 stores the value 5 in x. A double == is comparison: x == 5 checks whether x equals 5. Mixing these up is a common source of bugs.
Python also supports chained comparisons, a feature that reads like natural language:
chained.py
x = 7
1 < x < 10 # True (x is between 1 and 10)
1 < x < 5 # False
0 <= x <= 100 # TrueThis is equivalent to 1 < x and x < 10, but the chained form is more readable.
12.6.3 The in Operator
The in operator tests whether a value exists inside a collection:
in_operator.py
"a" in "apple" # True
"z" in "apple" # False
5 in [1, 2, 3, 4, 5] # TrueYou’ll use in constantly in Python, especially with collections (Chapter 13) and loops.
12.6.4 Exercises
Write an expression that computes the total cost of 250 units at $14.99 each, with a 6% sales tax. Format the result as currency using an f-string with comma separators and 2 decimal places.
A production line runs for
total_minutes = 497minutes. Use floor division and modulo to compute the hours and remaining minutes. Print the result as"8h 17m"(with your actual computed values).Predict the result of each expression, then verify:
10 / 5,10 // 5,10 % 5,7 % 2,2 ** 10. Pay attention to types.Given
name = "Northwind Traders", use theinoperator to check if"wind"appears in the name. Then use string methods to check if the name starts with"North"and ends with"Inc".
1. f"Total: ${14.99 * 250 * 1.06:,.2f}" → "Total: $3,972.35"
2.
solution.py
total_minutes = 497
hours = total_minutes // 60
minutes = total_minutes % 60
print(f"{hours}h {minutes}m") # 8h 17m3. 10 / 5 → 2.0 (float, division always returns float), 10 // 5 → 2 (int), 10 % 5 → 0 (int), 7 % 2 → 1 (int), 2 ** 10 → 1024 (int).
4.
solution.py
name = "Northwind Traders"
"wind" in name # True
name.startswith("North") # True
name.endswith("Inc") # False12.7 Python’s Boolean System
This section covers one of the most important and most commonly misunderstood aspects of Python. Every value in Python, not just True and False, has a truthiness: it evaluates to either True or False when used in a boolean context like an if statement.
12.7.1 Truthy and Falsy Values
The following values are falsy, meaning they evaluate to False:
| Value | Type | Why it’s falsy |
|---|---|---|
False |
bool |
Obviously |
0 |
int |
Zero is false |
0.0 |
float |
Zero is false |
"" |
str |
Empty string |
[] |
list |
Empty list |
{} |
dict |
Empty dictionary |
() |
tuple |
Empty tuple |
set() |
set |
Empty set |
None |
NoneType |
No value |
Everything else is truthy: it evaluates to True. Non-zero numbers, non-empty strings, non-empty collections, and most objects are all truthy.
truthiness.py
# All of these are truthy
bool(1) # True
bool(-1) # True (any non-zero number)
bool(0.001) # True
bool("hello") # True (any non-empty string)
bool(" ") # True (a space is not empty!)
bool([0]) # True (a list with one element, even if that element is 0)That last example is important: a list containing 0 is truthy because the list is non-empty. The truthiness of a collection depends on whether the collection itself is empty, not on the values it contains.
12.7.2 Short-Circuit Evaluation
Python’s and and or operators don’t just return True or False. They return one of their actual operands, and they use short-circuit evaluation, meaning they stop as soon as the result is determined.
and returns the first falsy value it encounters, or the last value if all are truthy:
and_operator.py
"hello" and "world" # "world" (both truthy, returns the last one)
"hello" and 0 # 0 (0 is falsy, returns it)
"" and "world" # "" (empty string is falsy, returns it)
0 and "" # 0 (0 is falsy, stops immediately)or returns the first truthy value it encounters, or the last value if all are falsy:
or_operator.py
"hello" or "world" # "hello" (first is truthy, returns it immediately)
"" or "world" # "world" (first is falsy, tries second)
0 or "" # "" (both falsy, returns the last one)
0 or None # None (both falsy, returns the last one)This behavior enables a common Python pattern for default values:
default_value.py
user_input = "" # Imagine the user left a field blank
name = user_input or "Anonymous"
print(name) # "Anonymous"Since the empty string is falsy, or moves on to the second operand and returns "Anonymous". If user_input had been "Alice", or would have returned "Alice" immediately without ever looking at the second operand.
or default pattern and zero
Be careful with value or default when value might legitimately be 0 or 0.0:
or_gotcha.py
quantity = 0
result = quantity or 10 # 10, not 0!Since 0 is falsy, or treats it the same as “missing” and returns 10. If 0 is a valid value, you need an explicit check instead:
explicit_check.py
result = quantity if quantity is not None else 1012.7.3 The not Operator
not inverts truthiness:
not_operator.py
not True # False
not False # True
not 0 # True (0 is falsy, so not 0 is True)
not "hello" # False ("hello" is truthy, so not "hello" is False)
not None # True (None is falsy)You’ll see not most often in conditions: if not results: is a concise way to check whether a list is empty.
If you’re thinking that truthiness feels like SQL’s NULL handling, you’re right. SQL’s NULL propagates through expressions in surprising ways, and Python’s None has its own quirks. The pattern of “checking for nothingness” is a recurring theme across programming languages, and once you learn to watch for it, you’ll recognize it everywhere.
12.8 Putting It Together: A Northwind Product Summary
Let’s combine everything from this chapter into a single script. Imagine you’re building a quick summary for a product in the Northwind database:
product_summary.py
# Product data (in later chapters, we'll read this from files and databases)
product_name = "Chai"
category = "Beverages"
unit_price = 18.00
units_in_stock = 29
units_on_order = 0
discontinued = False
reorder_level = 48
# Compute derived values
needs_reorder = units_in_stock <= reorder_level and not discontinued
total_value = unit_price * units_in_stock
# Format the summary
status = "DISCONTINUED" if discontinued else "Active"
reorder_flag = " [REORDER NEEDED]" if needs_reorder else ""
print(f"{'=' * 40}")
print(f"Product: {product_name}")
print(f"Category: {category}")
print(f"Status: {status}{reorder_flag}")
print(f"Unit Price: ${unit_price:,.2f}")
print(f"In Stock: {units_in_stock} units")
print(f"On Order: {units_on_order} units")
print(f"Stock Value: ${total_value:,.2f}")
print(f"{'=' * 40}")output
========================================
Product: Chai
Category: Beverages
Status: Active [REORDER NEEDED]
Unit Price: $18.00
In Stock: 29 units
On Order: 0 units
Stock Value: $522.00
========================================
This script uses variables, types, arithmetic operators, f-strings, comparison operators, boolean logic with and and not, and a conditional expression. It’s a small program, but it’s a complete one that produces useful output.
Exercises
Predict the Output
For each expression, predict what Python will return before testing it in the REPL. Then verify your prediction. Understanding why each result occurs matters more than getting the right answer.
predict.py
# 1
bool([])
# 2
bool([0, 0, 0])
# 3
"" or "default"
# 4
"hello" or "default"
# 5
0 and "hello"
# 6
1 and "hello"
# 7
None or 0 or "" or "finally"
# 8
not not "hello"
# 9
True + True + True
# 10
"hello" and "" and "world"Unit Conversion Script
Write a script called conversions.py that performs the following engineering unit conversions. Use well-named variables and include a comment citing the source or standard for each conversion factor.
- Convert 1500 PSI to kPa and MPa.
- Convert 72°F to Celsius and Kelvin.
- Convert 5.5 miles to kilometers and meters.
- Convert 2000 pounds-force to Newtons and kilonewtons.
Print each result using f-strings with appropriate precision (2 decimal places).
REPL Exploration
Open the REPL with uv run python and explore the following questions. Record your findings in a script called exploration.py as comments.
- What happens when you add a string and an integer? (
"hello" + 5) - What does
type(True + 1)return? What doesTrue + 1evaluate to? - What is
"hello" * 0? What about"hello" * -1? - Can you chain more than two comparisons? Try
1 < 2 < 3 < 4. - What does
type(None)return?
Northwind Product Card
Extend the product summary script to include three products from the Northwind catalog. For each product, define variables for product_name, unit_price, units_in_stock, discontinued, and reorder_level. Print a formatted summary card for each one. Include a “REORDER NEEDED” flag for products whose stock is at or below the reorder level (but only if the product isn’t discontinued).
Use these products (values from the actual Northwind database):
| Product | Price | In Stock | Reorder Level | Discontinued |
|---|---|---|---|---|
| Chai | 18.00 | 29 | 48 | No |
| Chang | 19.00 | 68 | 46 | No |
| Chef Anton’s Gumbo Mix | 21.35 | 45 | 37 | Yes |
Summary
This chapter established your Python environment and introduced the core building blocks of the language. You set up a project with uv init, learned to run scripts with uv run, and explored the REPL as a tool for quick experimentation. Python’s core types, int, float, str, bool, and None, map closely to the column types you’ve seen in SQL, and you’ll work with them constantly.
The most important takeaway from this chapter is how Python handles boolean logic. Every value has a truthiness, and and/or return actual operands rather than just True or False. This behavior powers common patterns like value or default that you’ll see throughout Python code. Understanding truthiness now will save you hours of debugging later.
In the next chapter, you’ll move from individual values to collections: lists, tuples, dictionaries, and sets. These are the data structures that let you work with groups of values, and they’ll connect directly to the rows and columns you know from SQL.
Glossary
- assignment
-
Storing a value in a variable using
=. Example:x = 42. - boolean
-
A type with only two values:
TrueandFalse. Used for logic and conditions. - comment
-
Text in source code that Python ignores, marked with
#. Used to explain the reasoning behind code. - dynamic typing
- A language feature where variables don’t have fixed types. The type is determined by the value currently assigned.
- expression
-
A combination of values, variables, and operators that Python evaluates to produce a result. Example:
2 + 3. - f-string
-
A formatted string literal, prefixed with
f, that can contain expressions inside curly braces. Example:f"Total: {x + y}". - falsy
-
A value that evaluates to
Falsein a boolean context. In Python:0,0.0,"",[],{},(),set(),None, andFalse. - float
- A number with a decimal point, stored using IEEE 754 double-precision format.
- floor division
-
Division that rounds the result down to the nearest integer, using the
//operator. - integer
- A whole number with no decimal point. Python integers have no size limit.
- modulo
-
The remainder after division, computed with the
%operator. None-
Python’s representation of “no value,” analogous to SQL’s
NULL. - operator
-
A symbol that performs an operation on values. Arithmetic (
+,-), comparison (==,<), and logical (and,or,not) are the main categories. - REPL
- Read-Eval-Print Loop. An interactive Python prompt for testing expressions.
- short-circuit evaluation
-
The behavior where
andandorstop evaluating as soon as the result is determined.andstops at the first falsy value;orstops at the first truthy value. - snake_case
- Python’s naming convention for variables and functions: lowercase words separated by underscores.
- string
- A sequence of characters, enclosed in single or double quotes. Strings are immutable.
- truthy
-
A value that evaluates to
Truein a boolean context. Most non-zero, non-empty values are truthy. - variable
- A name that refers to a value stored in memory.
12.3 Comments
Comments are lines in your code that Python ignores. They exist for humans, not for computers. In Python, comments start with the
#character:The purpose of comments is to explain why your code does something, not what it does. The code itself tells you what happens. The comment should tell you the reasoning behind it.
Here’s a bad comment:
This comment restates the code. Anyone who can read Python already knows that
x + 1incrementsx. The comment adds nothing.Here’s a good comment:
This comment explains the why: where the magic number
6.894757comes from and what standard it references. Six months from now, you (or a colleague) will understand this line immediately instead of wondering whether the constant is correct.12.3.1 Block Comments and Inline Comments
Block comments appear on their own line, above the code they describe. Inline comments appear on the same line as code, separated by at least two spaces:
Block comments are better for explaining why a section of code exists. Inline comments are better for brief clarifications, like units or edge cases.
12.3.2 TODO and FIXME
Two conventional comment markers help you track work in progress:
TODOmarks something you plan to implement later.FIXMEmarks a known bug or problem. Most code editors, including Zed, highlight these markers so they’re easy to find. They’re a lightweight alternative to formal issue tracking for small projects.12.3.3 When Code Is Self-Documenting
Well-chosen variable names often eliminate the need for comments entirely. Compare:
The second version needs no comment. The variable names tell the full story. When you find yourself writing a comment that restates the code, consider whether better naming would make the comment unnecessary.