Python Foundations: Variables, Types, and the Notebook
Suppose a file reader returns an order with the Python string values quantity "3" and price "12.50". In Python, multiplying that quantity by 2 produces "33", and multiplying the two values together raises an error. The quotation marks here denote Python string literals. A file reader may instead return numbers, depending on the format and its parsing rules; inspect the values it actually returns. Working with data in Python starts with three questions: where your code runs, what a name refers to, and what type of value it holds.
This article answers them in a Jupyter notebook. Each code block continues the same session, so run the blocks in order in one notebook; an interactive Python prompt also works. Outputs were checked with Python 3.12, and error messages can be worded slightly differently in other versions.
What happens when Python runs your code
A processor executes machine instructions encoded as binary numbers. Writing those directly is slow and tied to one kind of processor. A high-level language such as Python lets you write instructions as readable text made of names, numbers, and operators. The Python interpreter reads that text and carries out the instructions, so many programs can run on Windows, macOS, or Linux. A compatible Python version, required packages, and any platform-specific file paths or features still matter.
Python is open-source software with a large standard library and many third-party packages for tasks such as reading tables, drawing charts, and training models, so much of the work is combining existing code. Each line you write usually states one small step. Larger programs are built from many such steps, and whether they behave as expected depends on the basics in this article: what a name refers to, what type a value has, and when a line actually runs.
A notebook runs code in cells
You can run Python in several ways. Typing python in a terminal opens an interactive prompt that runs each statement as soon as you enter it. A script is a .py file that Python runs from top to bottom every time. A Jupyter notebook sits between the two: code is split into cells that you can edit and run one at a time, and each result appears below its cell.
| Where code runs | How it runs | Suited to | What to watch |
|---|---|---|---|
| Interactive prompt | Each statement runs as soon as it is entered. | Quick checks | Earlier lines are hard to edit and rerun; the session is not a written record. |
Script (.py) | The whole file runs from the top on every run. | Repeatable jobs | Seeing the effect of one change means running the whole file again. |
Notebook (.ipynb) | Cells run individually, in whatever order you choose. | Exploring data and explaining results | Results depend on the order in which cells were run. |
A notebook also has Markdown cells for headings, explanations, tables, and formulas, so the question, the code, its output, and your interpretation stay together. Behind the page, a kernel process runs the code and retains the top-level names created by cells until they are reassigned, deleted, or the kernel ends. That memory makes a notebook flexible, and it is also why the order of runs matters, as the final example in this article shows.
In a notebook, the value of a cell’s last line is displayed automatically when it is an expression. The examples use print() instead, which displays each value it is given in any setting, including scripts.
To begin, follow the official Jupyter installation guide for your Python environment, then launch jupyter notebook from its terminal. Create a notebook with a Python 3 kernel, put the first block below in a Code cell, and press Shift+Enter to execute it and move to the next cell. Lines starting with # are comments showing expected output; Python does not execute them. Save the notebook as an .ipynb file. Saving the file preserves cells and outputs, not the kernel’s live memory.
Calculating and printing
print("orders loaded")
# orders loaded
print(2 * 3 + 4)
# 10
print(2 * (3 + 4))
# 14
print(7 / 2, 6 / 3)
# 3.5 2.0
print(7 // 2, 7 % 2, 2 ** 10)
# 3 1 1024
Text in quotation marks is printed as written. For the built-in integers and floats used here, / produces a float, even for 6 / 3. // floors the quotient toward negative infinity: -7 // 2 is -4, not -3. A float operand can give a float result such as 7.0 // 2 == 3.0. % gives the remainder, and ** raises to a power. Python applies ** first, then *, /, //, and %, then + and -. Parentheses override that order, which is why the second and third lines differ.
Variables are names bound to values
A variable is a name that refers to a value. The assignment price = 12.5 evaluates the right-hand side, then binds the name on the left to the result. Afterward, writing price means “the value this name currently refers to.”
price = 12.5
quantity = 3
total = price * quantity
print(total)
# 37.5
quantity = 4
print(total)
# 37.5
total = price * quantity
print(total)
# 50.0
Changing quantity did not change total. The expression price * quantity was evaluated once, and total was bound to its result, 37.5. Unlike a spreadsheet formula, an assignment is not a live link. To get a new total, run the calculation again and assign its result.
Variables are often pictured as labeled boxes holding values. That picture helps when reading simple code, but a label attached to a value is more accurate. Two names can refer to the same object, and when an object can be changed in place, as a list can, the change is visible through every name that refers to it. Numbers and text cannot be changed in place, so the examples here behave the same under either picture.
A single = assigns. A double == compares two values and produces True or False. Writing = where Python expects a comparison, as in if quantity = 4:, is a SyntaxError.
print(quantity == 4)
# True
print(quantity == "4")
# False
quantity == "4" is False because the integer 4 and the text "4" are different values. Nothing warns you; the comparison simply answers no. When a value comes from a file or a form, check its type before comparing it.
Names Python accepts and people can read
For ASCII names, Python permits a letter or underscore first, followed by letters, digits, or underscores. Python also accepts many Unicode names, including Korean; isidentifier() checks the full character rules. Spaces and symbols such as &, -, and parentheses are not allowed; parentheses already call functions and group calculations. Names are case-sensitive, so age, Age, and AGE are three different names. Keywords such as if, for, in, and else belong to Python’s grammar and cannot be used as names; most editors color them differently.
import keyword
print("max_age".isidentifier(), "_order2".isidentifier())
# True True
print("1st_order".isidentifier(), "apples&oranges".isidentifier())
# False False
print("for".isidentifier(), keyword.iskeyword("for"))
# True True
isidentifier() checks only the character rules, so "for" passes it; keyword.iskeyword() reports that for is reserved. The first line of the block, import keyword, loads the keyword module from Python’s standard library.
Some names are legal but risky. print, str, list, max, and type include built-in functions and types. The assignment str = "order-17" is allowed, but afterward str(42) fails with TypeError: 'str' object is not callable, because str now refers to your text. In a notebook, the built-in stays hidden until you remove your name with del str or restart the kernel.
Beyond the rules, conventions make code readable. Python code usually uses lowercase words joined by underscores, such as max_age and order_total. A name should say what its value means: x tells a reader nothing, while ages and max_age do.
ages = [31, 27, 34]
max_age = max(ages)
min_age = min(ages)
print(max_age, max_age - min_age)
# 34 7
Square brackets create a list of values. The built-in functions max() and min() return its largest and smallest values, and the names record what each result means, so max_age - min_age reads as the age range it computes.
Types decide what an operation means
Every value has a data type, and the type determines what operations mean for that value. type() reports it. Python describes a type as a class, as in <class 'int'>; a class defines what its values are and what they can do.
print(type(34), type(34.0), type("34"))
# <class 'int'> <class 'float'> <class 'str'>
print(type(True), type(None))
# <class 'bool'> <class 'NoneType'>
int: integers whose size can grow as needed, subject to available memory, such as34or-2.float: binary floating-point numbers, including whole-valued34.0. Values such as2.5are exact, while many decimal fractions such as0.1are approximated.str: text in single or double quotation marks;'data'and"data"are the same value.bool: the two valuesTrueandFalse, produced by comparisons.None: a single value, of typeNoneType, that marks the absence of a value.
The type belongs to the value, not to the name. After max_age = "34", the same name would refer to text. This is dynamic typing: you do not declare a variable’s type, and Python checks types when an operation runs.
print(2 + 3, "2" + "3", 1 + 2.5, "3" * 2)
# 5 23 3.5 33
+ adds numbers but joins text, and * with text and an integer repeats the text. Neither is an error, which is how a number stored as text can silently produce a wrong result. print() shows text without its quotation marks, so the 23 and 33 in the output are text, not numbers.
Reading a type error
Some combinations have no agreed meaning, and Python stops with an error instead of guessing. 7 + "8" could mean 15 or "78", so Python refuses it.
try:
7 + "8"
except TypeError as error:
print("TypeError:", error)
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
Without try, the notebook would stop the cell and show a traceback. Its upper lines point to where the error happened, and its last line names the error type and the problem. Here, try runs the risky line and except TypeError catches that one kind of error, so the session continues and prints the message.
Read the message as evidence: 'int' and 'str' tell you which types were combined. Searching for the exact error type and message is normal practice; compare any suggested fix with your own values and types before using it.
Converting types deliberately
Python converts between some numeric types automatically. In 1 + 2.5, the integer is treated as a float, and the result is 3.5. Python does not convert text to numbers on its own. Explicit conversion, also called casting, calls the target type: int(), float(), or str().
quantity_text = "3"
price_text = "12.50"
quantity = int(quantity_text)
price = float(price_text)
print(quantity * price)
# 37.5
str(quantity)
print(type(quantity))
# <class 'int'>
label = "qty=" + str(quantity)
print(label)
# qty=3
In this cell, str(quantity) returns the text "3" without assigning it. It is not the last expression in the cell, so it is not automatically displayed; quantity is still an integer. These conversions return a result without rebinding the input name. Assign the result if you want a name for it, as the label line does. This is different from an operation that mutates a shared list in place; mutation does not require rebinding its name.
try:
int("2.5")
except ValueError as error:
print("ValueError:", error)
# ValueError: invalid literal for int() with base 10: '2.5'
print(int(float("2.5")), int(2.9), int(-2.9))
# 2 2 -2
print(bool("False"), bool(""))
# True False
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
# 0.30000000000000004 False
int("2.5") fails because the text is not written as a whole number. Converting through float first succeeds, but int() truncates toward zero, so 2.9 becomes 2 and −2.9 becomes −2. Whether a quantity of 2.5 should be rejected, rounded, or truncated is a rule about the data, not something to leave to a Python default. Text such as "1,200" or an empty string fails both int() and float(). bool() treats every non-empty string as true, including "False".
The last line shows that a float is a binary approximation: 0.1 + 0.2 is not exactly 0.3. That is acceptable for many measurements, but not for exact money amounts, where production code usually uses decimal arithmetic.
Execution order in a notebook
Because the kernel keeps names in memory, a cell uses whatever values exist when it runs, not the values written above it on the page. Suppose a notebook has three cells: A sets discount = 0.1, B computes and prints a final price, and C sets discount = 0.2. Running A, B, and C in order prints 11.25 from B. Running B again afterward uses the value from C. The block below replays that sequence of runs:
discount = 0.1
final_price = price * (1 - discount)
print(final_price)
# 11.25
discount = 0.2
final_price = price * (1 - discount)
print(final_price)
# 10.0
The page still shows cell B above cell C, yet B’s latest output came from C’s value. Within a single kernel session, execution numbers help reconstruct the most recent runs; saved numbers can also remain from an earlier session. Saved outputs are records of earlier runs, not proof that the current code produces them. Before sharing a notebook, restart the kernel and run all cells from the top. In this fixed-input example, the changed result exposes an execution-order dependency. In a real notebook, changed data, random draws, time, or package versions can also change results. A successful clean run checks one execution path; it does not establish that the analysis is correct.
Notebooks suit exploring data and explaining results. When code must run on a schedule without anyone watching, it usually moves into scripts or packages with tests, with an explicit entry point and recorded inputs.
Practice
1. Predict the output of these four lines, then run them: a = 5, b = a, a = a + 1, print(a, b).
Solution
6 5. b = a binds b to the value 5. a = a + 1 computes the new value 6 and rebinds only a, so b still refers to 5.
2. A file provides quantity = "4" and price = "9.99". The expression quantity + price runs without an error. What does it produce, and how do you compute the order total?
Solution
It joins the two strings into the text "49.99". Convert both values before calculating:
quantity = "4"
price = "9.99"
print(quantity + price)
# 49.99
total = int(quantity) * float(price)
print(round(total, 2))
# 39.96
The absence of an error did not mean the first result was correct. round(total, 2) returns a rounded numeric value; it is not merely display formatting and does not reassign total. Its result is still a float. For exact monetary rules, use decimal arithmetic constructed from the source strings or integer minor units, with an explicit rounding policy.
3. A notebook has three cells: A sets threshold = 100, B computes flag = total > threshold, and C sets threshold = 50. total is 80. You run A, then C, then B. What is flag? What will a colleague get after restarting the kernel and running all cells, and how would you remove the ambiguity?
Solution
Your run gives True, because B used 50. Running top to bottom, B runs before C and gives False. Define the threshold once, before it is used, or give different settings different names. Then restart the kernel and run all cells to confirm that the page order produces the result you report.
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
