BeginnerPython · 27 min · 22 challenges

Python Expressions: Beginner

An engineer at Spotify building one listener's Wrapped summary has minutes = 48213 and a template string in the pipeline, and the line that glues them together reads "You listened for " + minutes. Whether that line prints a greeting or stops the whole job depends on what Python does when text meets a number.

How Computers Store Data

Daily Life
Interviews

Run price = 10 and Python does 3 things. It builds an integer object holding 10 somewhere in memory, it records that the object's type is int, and it attaches the name price to that object. The name is a label stuck on the outside; the object is the thing that takes up space. Keeping the label and the object apart explains almost everything else in this lesson.

Bits, bytes and objects

Memory is a long row of numbered slots called bytes, and every value Python builds occupies a run of consecutive bytes starting at some address. The object stores more than the raw number: it carries a type tag so Python knows how to read the bytes, a count of how many names point at it, and then the value itself. That bookkeeping is why a Python integer takes more room than the 8 bytes a C program would spend on the same number.
ValueTypeBytes it occupies
42int28
3.14float24
Truebool28
"hi"str51
""str49

Sizes as reported by CPython 3.9 on a 64-bit machine. A 2-character string costs 2 bytes more than an empty one; the other 49 bytes are the object's bookkeeping.

8
bits per byte
256
patterns a byte can hold
64
bits in every float

A float always fits in the same fixed number of bits, so there's a largest value it can hold and a limit to how many digits it keeps. An int has no such ceiling. When a whole number outgrows the bytes it started with, Python allocates a bigger object for it, so arithmetic on huge integers stays exact.

exact = 2 ** 100 + 1
rough = 2.0 ** 100 + 1
print(exact)
print(rough)
print(exact - 2 ** 100)
print(rough - 2.0 ** 100)
>>>Output
1267650600228229401496703205377
1.2676506002282294e+30
1
0.0

The integer keeps all 31 digits, including the 1 added at the end. The float prints as 1.2676506002282294e+30, meaning 1.2676506002282294 times 10 to the power 30. Subtracting 2.0 ** 100 back out leaves 0.0, so the added 1 never reached the stored value. A float holds about 16 significant digits and spends them on the leading part of the number, which means the tail is rounded away. In data work, counts of rows and bytes are whole numbers, and so are timestamps, which is why they're kept as int for as long as possible.

A name points at an object

Assignment with a name on the right side copies the arrow, never the object. After saved = price, both names point at the same integer, and nothing has been duplicated. When price is later assigned 20, Python points price at an object holding 20: a pre-built one for a small number like this, a freshly built one for a larger number. Either way only the price arrow swings, and saved keeps pointing where it always did. Step through the run below and watch each name move.

1price = 10
2saved = price
3price = 20
4print(price)
5print(saved)
6label = "ok"
7label = label + "!"
8print(label)
price=
saved=
label=
output0 lines
price starts at 10
1/9

Reassigning price builds a new object; saved keeps pointing at the old one.

The final 3 lines make the same point with text. label + "!" builds a brand new string "ok!", then the name label is moved onto it. The original "ok" object is unchanged, because + builds its result separately and never edits the string it read from. An object that nothing refers to any more is freed, and its bytes are reused.

What a variable holds
  • ▸A variable holds a reference to an object, never the object's bytes themselves
  • ▸Assigning one name to another makes both refer to the same object
  • ▸Reassigning a name points it at a different object and leaves the old one alone
  • ▸Every object knows its own type; names carry no type at all
Multiple Choice

What does this program print?

a = 5
b = a
a = a + 1
print(b)

In stock = stock - 5 the same name sits on both sides. Python evaluates the right side first, using the object stock points at now, and only then moves the name onto the result. Tracing a program means noting where each name points after every line, as in this table.

Linepointsstart
points = 3030not yet assigned
start = points3030
points = points + 83830

Variables and Naming

Daily Life
Interviews

A name is the label you stick on an object. Python lets you spell it with letters and digits plus the _ character, as long as the first character is not a digit. Upper and lower case count as different characters: total is one name, and Total and TOTAL are 2 more. A space ends a name, as does a hyphen or any other punctuation, which is why order total reads as 2 names and order-total reads as a subtraction.

Which spellings Python accepts

NameAccepted?Why
row_countyesletters and an underscore
row2yesa digit may appear after the first character
_cacheyesan underscore may start a name
2nd_rownoa name cannot begin with a digit
row-countnothe hyphen is read as subtraction
row countnothe space splits it into 2 names
classnoa keyword Python reserves for itself
Classyeskeywords are case sensitive, so this spelling is free

The last 2 rows point at a short list of words with fixed meaning in the language. True, False, None, and, or, not, if, for, while, def, class, import, return are on it, and the list has a few more. Assigning to one of them stops the program before it starts, with a SyntaxError on that line.

membership_class = "gold"
class_ = "gold"
35
keywords Python 3.9 reserves
3
character kinds allowed in a name

Case matters, and so does spelling

Because Total and total are unrelated names, a typo in one place produces a name that was never assigned. Python reports that the moment the line runs, after any earlier lines have already produced their output.

Total = 40
print(Total)
print(total)

Names that already mean something

print is a name too, attached at startup to the built-in object that writes to the screen. Nothing stops you from reattaching it to an integer. The assignment succeeds, and the very next call fails, because an integer can't be called.

print = 5
print("hello")
TIP
print and type are built-in names, not reserved words, and so are max and sum, so Python lets you overwrite any of them. Treat them as taken anyway: pick a name like max_price instead of max, and the built-in stays usable for the rest of the program.

Conventions that make names readable

Python only enforces the spelling rules above. The rest is convention, followed almost universally so that code reads the same across teams. Variable names are lowercase words joined by the _ character, which is called snake case. A value meant to stay fixed for the whole program is spelled in capitals, such as MAX_RETRIES = 3, a signal to readers rather than a lock. And a name says what the value is, so that a line can be read without scrolling up to the assignment.

✓Names that hide the meaning
  • x = 1200
  • tmp = x * 12
  • d = 30
  • flag = tmp == 14400
✗Names that carry it
  • monthly_rent = 1200
  • yearly_rent = monthly_rent * 12
  • days_in_month = 30
  • is_full_year = yearly_rent == 14400

In pipeline code the strongest names say where a value came from and what unit it is in. orders_raw_count and orders_clean_count can sit side by side without confusion, and timeout_seconds can't be mistaken for milliseconds. A name that took 5 extra characters to type saves the next reader a trip through the whole file.

✓Do
  • Use snake_case for variables: rows_loaded, not rowsLoaded
  • Spell constants in capitals: BATCH_SIZE = 500
  • Name the unit when it matters: timeout_seconds, size_mb
  • Make booleans read as a question: is_ready, has_header
✗Don't
  • Reuse one letter for several things: x for a price and later for a count
  • Shadow a built-in: list, sum, max, min, print
  • Encode the type in the name: str_name, int_count
  • Rely on case to tell 2 names apart: Total and total
Multiple Choice

Which line will Python refuse to run?

A NameError quotes the exact spelling Python looked up and failed to find. Compare it character by character with the assignment that was meant to create it: a single capital letter makes a separate name. The misspelled assignment ran without complaint, so the error appears later, at the first line that reads the name.

Assignment vs. Equality

Daily Life
Interviews

A single = is a statement: it takes the object produced by the expression on its right and binds the name on its left to it. It produces no value of its own, which is why = can never sit inside a larger expression. A double == is an operator: it compares the objects on both sides and produces a bool, which is either True or False. That result is an ordinary value. You can print it or store it, and another operator can take it as input.

LineKindWhat happensValue produced
count = 5statementbinds count to the object 5none
count == 5expressioncompares count with 5True
count == 6expressioncompares count with 6False
same = count == 5statementevaluates the comparison, then binds same to the resultnone

Read = aloud as "is set to" and == as "is equal to", and most mistakes disappear before they're typed. The run below changes actual twice and compares it after each change. Notice that the comparison on line 3 is evaluated once, when that line runs, and the stored check does not update when actual changes later.

1expected = 3
2actual = 3
3check = actual == expected
4print(check)
5actual = 4
6print(actual == expected)
7print(check)
8actual = expected
9print(actual == expected)
expected=
actual=
check=
output0 lines
expected starts at 3
1/10

check keeps the True it was given on line 3, whatever actual does afterwards.

Line 7 prints True even though actual is 4 by then, because check refers to the bool object built on line 3, and nothing has rebound it. Line 8 makes actual point at the very object expected points at, so the final comparison is True again.

Equality looks at values, and at types

== asks whether 2 objects represent the same value. Numbers compare by their numeric value, so an int can equal a float. True equals 1 as well, because a bool is an integer underneath. A string never equals a number, even when the digits match, because the text "10" and the integer 10 are different kinds of object, and == between them reports False rather than failing.

score = 10
print(score == 10)
print(score == 11)
print(score == 10.0)
print(score == "10")
print(True == 1)
>>>Output
True
False
True
False
True

That fourth line is the one that bites in data work. A value read from a CSV column arrives as text, and comparing it with a number is legal and always False, so a check that looks correct passes nothing. The later lesson on type conversions shows how to turn the text into a number before comparing.

What the left side of = may be

The left side of an assignment names a place to attach the object. A literal like 5 or an expression like score + 1 is not a place, so Python rejects the whole program before running a single line.

LineWhat Python reports
5 = scoreSyntaxError: cannot assign to literal
score + 1 = 11SyntaxError: cannot assign to operator
score + 1 == 11accepted: a comparison, True when score is 10

Storing the answer to a question

Because == produces a value, a comparison can be stored under a name and reused. Name the stored bool as a question so the later line reads naturally, and remember that it records the answer at the moment it was computed.

seats = 180
booked = 180
is_full = booked == seats
print("Full flight:", is_full)
booked = 179
print("Full flight:", is_full)
print("Full flight:", booked == seats)
>>>Output
Full flight: True
Full flight: True
Full flight: False
TIP
A stored comparison is a snapshot. If the inputs can change, compare again at the point where the answer matters instead of trusting a bool computed several lines earlier.
Multiple Choice

What does this program print?

limit = 100
usage = 100
over = usage == limit
usage = 150
print(over)

Before writing a line, decide what it is for: changing what a name points at, or producing an answer. Storing an answer uses = at the left with == on its right side. Rebinding a name uses a single = alone. == compares numeric value, so it answers True when an int meets a float that holds the same number.

Mixing Types in One Expression

Daily Life
Interviews

Every operator looks at the type of the object on each side before it does anything. For +, 2 numbers add and 2 strings join. A number next to a string has no meaning, so Python stops with a TypeError instead of guessing. The table lists what each pairing produces.

LeftOperatorRightResultExample
int+ - * **intint3 + 4 gives 7
int+ - * **floatfloat3 + 4.0 gives 7.0
int/intfloat8 / 2 gives 4.0
int//floatfloat7 // 2.0 gives 3.0
bool+ - *intintTrue + 1 gives 2
str+strstr"3" + "4" gives "34"
str*intstr"ab" * 3 gives "ababab"
str+intTypeError"3" + 4 fails
str*strTypeError"3" * "4" fails
str- /strTypeError"10" - "1" fails

Numbers and booleans mix freely

Among numeric types, the result takes the wider type. An int beside a float produces a float, and / produces a float even when both sides are int and the division is exact. A bool behaves as the integer 1 or 0 inside arithmetic, which is occasionally handy and more often a sign that the wrong value reached the expression.

print(3 + 4.0)
print(8 / 2)
print(7 // 2)
print(7 // 2.0)
print(True + 1)
print(False * 10)
print(1 + 2.5 + True)
>>>Output
7.0
4.0
3
3.0
2
0
4.5

Text has its own arithmetic

+ between 2 strings joins them, and * between a string and an int repeats it. Neither one does any numeric work: "3" + "4" produces the 2-character text "34", because both operands are text. A bool works as the repeat count too, since it is an int underneath.

print("3" + "4")
print("ab" * 3)
print("-" * 12)
print("x" * True)
print("x" * False)
print("done")
>>>Output
34
ababab
------------
x
done

The fifth line is empty: a string repeated 0 times is the empty string, and printing it still ends the line.

The combinations that fail

Python raises a TypeError when a string sits beside a number under +. It does the same for - or * between 2 strings, and for any / involving text. The program runs normally up to that line, prints whatever it printed, and stops. The message names both types, which tells you which operand carried the wrong kind of value.

minutes = 48213
print("Building summary")
message = "You listened for " + minutes
print(message)
The order of the operands changes the message, because Python asks the left object first. Every outcome in the table is the same stop.
ExpressionMessage
"3" + 4TypeError: can only concatenate str (not "int") to str
4 + "3"TypeError: unsupported operand type(s) for +: 'int' and 'str'
"3" * "4"TypeError: can't multiply sequence by non-int of type 'str'
"10" - "1"TypeError: unsupported operand type(s) for -: 'str' and 'str'
True + "!"TypeError: unsupported operand type(s) for +: 'bool' and 'str'

Let print do the joining

print takes any number of values separated by commas, converts each one to text on its own, and writes them with a space between. That is the route for showing a number next to a label without an operator in between. Turning a number into a string yourself is covered in a later lesson on type conversions.

count = 5
print("Rows:", count)
print("Rows: " + count)
Before an operator runs
  • ▸Both numbers: the result takes the wider type, and / always gives a float
  • ▸bool with a number: True counts as 1, False as 0
  • ▸str + str joins, str * int repeats
  • ▸str with int under +, or str with str under - * /: TypeError, program stops
Multiple Choice

What does this line print?

print(True + True + "!")

The comma route adds exactly 1 space between arguments, on top of any spaces inside the text. A label written for joining with + usually ends in a space, so drop it when you switch, or the output shows 2.

files = 9
print("Files:", files)
print("Files: ", files)
>>>Output
Files: 9
Files: 9

The second output line has 2 spaces between the colon and the 9: one from the label and one added by print. When a + line fails because one side is a number, replace it with a print call that passes the label and the value as separate arguments. Trim the label's trailing space in the same edit.

Each operator needs the pair it accepts. A repeat count stands on the other side of * from the string: "-" * 4 gives ----, while "-" * "4" fails. The same int added to a float gives a float, so 4 + 0.5 is 4.5.

Operators and Readability

Daily Life
Interviews

When an expression holds several operators, Python applies them in a fixed order rather than left to right across the whole line. ** goes first. * and / share the next level with // and %. Below them sit + and -, and == comes last. Within one level, evaluation runs left to right, except for **, which runs right to left. Parentheses override all of it, and they're also the only way to tell a reader what you meant.

LevelOperatorsDirectionExample
1 (first)( )innermost first(2 + 3) * 4 gives 20
2**right to left2 ** 3 ** 2 gives 512
3- (sign)applies to the operand beside it-2 ** 2 gives -4
4* / // %left to right100 / 10 / 2 gives 5.0
5+ -left to right10 - 4 - 3 gives 3
6 (last)==after all arithmetic2 + 3 == 5 gives True
print(2 + 3 * 4)
print((2 + 3) * 4)
print(2 ** 3 ** 2)
print(-2 ** 2)
print(10 - 4 - 3)
print(100 / 10 / 2)
print(2 + 3 == 5)
>>>Output
14
20
512
-4
3
5.0
True

Line 3 is the surprising one: 2 ** 3 ** 2 is 2 ** 9, which is 512, while (2 ** 3) ** 2 would be 64. Line 4 is the other: the exponent binds tighter than the minus sign, so the result is the negative of 4. Both are legal, and both are the kind of line a colleague will misread, which is the reason to add parentheses even where Python does not need them.

Parentheses say what you meant

The run below computes a bill 2 ways. The first adds tax on top of the subtotal, the second doubles the subtotal and takes a quarter of that, and the only difference on the page is a pair of parentheses.
1price = 20
2qty = 3
3tax_rate = 0.25
4subtotal = price * qty
5total = subtotal + subtotal * tax_rate
6print(total)
7wrong = (subtotal + subtotal) * tax_rate
8print(wrong)
price=
qty=
tax_rate=
subtotal=
total=
wrong=
output0 lines
price starts at 20
1/9

Multiplication runs first in total; the parentheses force the addition first in wrong.

Name the steps

A long expression is right or wrong in one go, and a reader has to hold every intermediate value in their head to check it. Breaking it into named steps gives each value a name, lets you print any step while you're investigating, and makes the operator order a smaller concern because each line holds 1 or 2 operators at most. The 2 programs below print the same number, 37.5.
print((20 * 3 + 20 * 3 * 0.25) * (1 - 0.5))
unit_price = 20
quantity = 3
tax_rate = 0.25
discount = 0.5
subtotal = unit_price * quantity
with_tax = subtotal + subtotal * tax_rate
total = with_tax * (1 - discount)
print(total)

Spacing and layout

Python ignores spaces inside an expression: a*b+c runs the same as a * b + c. The convention is 1 space on each side of = and ==. The lower-precedence operators get the same spacing, while a higher-precedence operator loses its spaces so the grouping shows: a*b + c. A line longer than about 80 characters gets split by naming part of it, as above.

✓Do
  • Add parentheses wherever 2 operators of different levels meet
  • Put 1 space around =, == and the + or - of an expression
  • Name an intermediate result if the line needs explaining
  • Keep ** and a minus sign apart with parentheses: (-2) ** 2 or -(2 ** 2)
✗Don't
  • Rely on readers knowing that ** runs right to left
  • Pack 6 operators into one print call
  • Mix spaced and unspaced operators at the same level: a+b * c
  • Reuse a name for a different quantity 2 lines later
Multiple Choice

What does this program print?

base = 10
bonus = 5
years = 2
print(base + bonus * years ** 2)

Capstone practice

Each program below pulls on more than one section at once, so expect naming and the assignment-versus-comparison question to turn up in the same lines as type mixing and operator order.

A fixed fee plus a per-item cost needs no parentheses, because * runs before +: 5.0 + 4 * 1.5 multiplies to 6.0 first and sums to 11.0. Swap the 2 operators and 5.0 * 4 + 1.5 gives 21.5. Working the line through with real numbers shows whether each operator sits where it belongs.

String operators share the same precedence. In "-" * 3 + "x" the repetition runs first and produces ---. The join then gives ---x. Writing "x" + "-" * 3 gives x--- for the same reason: the * on the right finishes before the join begins.

❯❯❯PUTTING IT ALL TOGETHER

> Back at Spotify, the Wrapped job holds minutes = 48213 and builds its headline with "You listened for " + minutes. The run prints its first log line and then stops with a TypeError at line 3, before a single listener's summary is written. The engineer swaps the join for a comma-separated print with the label "You listened for", and adds a check that the per-listener total equals the sum of its daily parts, stored under totals_match and computed after the last daily value is added rather than before.

The integer 48213 lives as one object in memory, and every name that refers to it, including the one passed to print, points at that same object rather than a copy.
The name minutes states its unit, so nobody reading the headline line wonders whether 48213 counts seconds or songs.
"You listened for " + minutes fails because + sees a str on the left and an int on the right; print with a comma converts each argument on its own, and dropping the label's trailing space keeps a single space in the output.
totals_match = per_listener == daily_sum is a snapshot, so it's placed after the final daily value is added; computed any earlier, it would keep an answer that no longer matches the total.
The daily sum is written as named steps rather than one long line, so the operator order never has to be reasoned about during a 3am incident.
KEY TAKEAWAYS
A variable is a name attached to an object in memory; assigning one name from another copies the arrow, and reassigning moves only that one name.
Names use letters and digits plus the _ character, and cannot start with a digit or be a keyword. Case matters, and the convention is snake_case with descriptive words.
A single = binds a name and produces nothing; a double == compares 2 values and produces a bool, computed once at the line where it appears.
Operators check types first. Numbers mix with bools, and strings support joining and repetition. A string beside a number under + stops the program with a TypeError.
** runs before * and /, which run before + and -, with == last; parentheses and named intermediate steps make the order visible to the next reader.

Values live in memory, names point at them, and an operator only works when the types on both sides agree.

Category
Python
Difficulty
beginner
Duration
27 minutes
Challenges
22 hands-on challenges

Topics covered: How Computers Store Data, Variables and Naming, Assignment vs. Equality, Mixing Types in One Expression, Operators and Readability

Lesson Sections

  1. How Computers Store Data (concepts: Variables)

    Bits, bytes and objects Memory is a long row of numbered slots called bytes, and every value Python builds occupies a run of consecutive bytes starting at some address. The object stores more than the raw number: it carries a type tag so Python knows how to read the bytes, a count of how many names point at it, and then the value itself. That bookkeeping is why a Python integer takes more room than the 8 bytes a C program would spend on the same number. Sizes as reported by CPython 3.9 on a 64-b

  2. Variables and Naming (concepts: Variables)

    Which spellings Python accepts Case matters, and so does spelling Names that already mean something Conventions that make names readable

  3. Assignment vs. Equality (concepts: Variables)

    Equality looks at values, and at types What the left side of = may be Storing the answer to a question

  4. Mixing Types in One Expression (concepts: Data Types)

    Numbers and booleans mix freely Text has its own arithmetic The fifth line is empty: a string repeated 0 times is the empty string, and printing it still ends the line. The combinations that fail The order of the operands changes the message, because Python asks the left object first. Every outcome in the table is the same stop. Let print do the joining

  5. Operators and Readability (concepts: Arithmetic)

    Parentheses say what you meant The run below computes a bill 2 ways. The first adds tax on top of the subtotal, the second doubles the subtotal and takes a quarter of that, and the only difference on the page is a pair of parentheses. Name the steps A long expression is right or wrong in one go, and a reader has to hold every intermediate value in their head to check it. Breaking it into named steps gives each value a name, lets you print any step while you're investigating, and makes the operat