n = 42
pi = 3.14159
flag = True
name = "Alice"
nothing = None
[type(x).__name__ for x in (n, pi, flag, name, nothing)]['int', 'float', 'bool', 'str', 'NoneType']
Python has a small set of fundamental types: int, float, bool, str, None. A variable is created by assignment — its type is determined by what it points to, not by any declaration.
In this chapter you will learn to:
//, %, ** operators.==) from identity (is) — and know when to use each.True in a condition.Before we can do anything interesting in a program, we need a vocabulary of basic values: whole numbers, fractional numbers, true/false flags, text, and “nothing”. Python gives you exactly those five and infers the type from the literal — no int x = 42; declaration step.
n = 42
pi = 3.14159
flag = True
name = "Alice"
nothing = None
[type(x).__name__ for x in (n, pi, flag, name, nothing)]['int', 'float', 'bool', 'str', 'NoneType']
42 is an int literal — a whole number.3.14159 is a float literal — the decimal point is the cue.True (and False) are bool literals — capitalized exactly."Alice" is a str literal — quoted text.None is the singleton NoneType value — Python’s “no value here” marker.type(x).__name__ asks each value for its class name, confirming the inference.The [expr for x in iterable] shape in that cell is a list comprehension — a compact way to apply the same expression to every item of a collection. We cover them properly in Chapter 14; here, just read it as shorthand for “do type(x).__name__ for each value, and put the answers in a list.”
The general rule: literals carry their own type; assignment never declares one. Notes worth keeping in mind:
int has no fixed size — 2 ** 200 is exact. Use underscores for readability: 1_000_000.float is a 64-bit IEEE-754 double. It is not exact: 0.1 + 0.2 is 0.30000000000000004. For exact decimal arithmetic, use decimal.Decimal (shown below).bool is a subclass of int: True == 1, False == 0. So sum([True, False, True]) is 2.str is full Unicode. The deep treatment is in Chapter 16.None is the absence of a value — it’s its own type, NoneType, and there is one None singleton.0.1 + 0.2, 0.1 + 0.2 == 0.3(0.30000000000000004, False)
The first value, 0.30000000000000004, is the actual result of adding two binary-floating-point approximations of 0.1 and 0.2 — neither input can be represented exactly in base-2, so the addition’s last bit is wrong. The second answer, False, is the consequence: 0.1 + 0.2 is genuinely not 0.3 to the bit-pattern level, even though every grade-school sanity check says it should be. This is why == on floats from arithmetic is a trap.
The fix is math.isclose(a, b), which compares within a tolerance instead of bit-for-bit:
import math
math.isclose(0.1 + 0.2, 0.3)True
For numbers from sources you don’t control, math.isclose is the right comparison — never ==. By default it uses a relative tolerance of 1e-09 (i.e. agreement to nine significant digits); pass rel_tol= or abs_tol= to tighten or loosen it.
When you cannot tolerate any rounding — money is the canonical case — reach for decimal.Decimal, which stores numbers in base-10 instead of base-2 and so represents 0.1 exactly:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") == Decimal("0.3")True
The string constructor (Decimal("0.1")) is intentional — passing the float 0.1 would import the same rounding error you were trying to avoid. Decimals are slower than floats, so use them only where exactness matters.
A variable comes into existence on first assignment. There is no declaration step.
name = "Alice"
age = 30
height = 1.75
result = None
[type(x).__name__ for x in (name, age, height, result)]['str', 'int', 'float', 'NoneType']
Naming conventions Python programmers follow without exception (PEP 8 — Python Enhancement Proposal 8, the official style guide that essentially every Python codebase follows for whitespace, naming, and layout):
snake_case for variables and functions.UpperCamelCase for class names.UPPER_SNAKE_CASE for module-level constants.Multiple assignment and unpacking are common. In most languages, swapping two variables needs a temporary: t = a; a = b; b = t. Python lets you write the swap as a single statement.
a, b = 1, 2
a, b = b, a
a, b(2, 1)
a, b = 1, 2 is tuple unpacking: Python builds the tuple (1, 2) on the right, then assigns the first element to a and the second to b.a, b = b, a does the same — but the right-hand side (b, a) is fully evaluated before any assignment happens, so the original values land on the new names with no temporary needed.The general rule: the right-hand side is computed first, then unpacked into the names on the left.
Python has four numeric operators worth distinguishing up front. Two of them — / and // — both look like division in other languages, but they answer different questions.
[7 / 2, 7 // 2, 7 % 2, 2 ** 10][3.5, 3, 1, 1024]
7 / 2 is true division — always returns a float, even when the inputs are integers and the result is whole. 8 / 2 is 4.0, not 4.7 // 2 is floor division — discards the fractional part, returning an int for int inputs.7 % 2 is the remainder that pairs with // — it satisfies (a // b) * b + (a % b) == a.2 ** 10 is exponentiation — 2 to the power of 10.The general rule: / for “give me the exact ratio as a float”, // and % together when you want quotient-and-remainder integer arithmetic. Floor division rounds toward negative infinity, not toward zero:
-7 // 2-4
The remainder % is consistent with //: (-7 // 2) * 2 + (-7 % 2) == -7.
In-place updates have shorthand: x += 1, x -= 1, x *= 2. They modify the variable and are equivalent to x = x + 1, etc.
round() uses banker’s rounding — ties round to the nearest even integer, not always up:
[round(0.5), round(1.5), round(2.5), round(3.5)][0, 2, 2, 4]
0.5 → 0, 1.5 → 2, 2.5 → 2, 3.5 → 4. This avoids the systematic bias you get from “round half up” when summing many rounded values. The deeper reason is the floating-point representation of 2.5; for exact decimal rounding, use decimal.Decimal.
Languages like C make if strict: only 0 is false. Python is more generous — and more useful. In an if or while, any value can be evaluated as a boolean. Python defines a small set of falsy values; everything else is truthy.
falsy = [False, None, 0, 0.0, "", [], {}, set(), ()]
[bool(x) for x in falsy][False, False, False, False, False, False, False, False, False]
False and None are explicit “no” values.0 and 0.0 — numeric zero, in int and float form."" — the empty string.[], {}, set(), () — empty list, empty dict, empty set, empty tuple. ({} is dict-empty because Python’s curly-brace literal stores key:value pairs; the empty set has no literal form, so you must write set().)bool(x) calls Python’s truthiness check on each, and they all answer False.The general rule: a value is falsy if it’s “empty” or “zero” by its own type’s reckoning; everything else is truthy. This shapes Pythonic style:
items = []
"empty" if not items else "has items"'empty'
The expression "empty" if not items else "has items" is Python’s ternary form — the conditional shortened into one expression. It reads almost as English: “give me empty if not items, else has items.” not items is True for an empty list (because bool([]) is False), so the first branch fires.
Same shape with a non-empty list:
items = [1, 2, 3]
"empty" if not items else "has items"'has items'
Now bool([1, 2, 3]) is True, so not items is False, and the else branch returns "has items". The point: the list itself — not its length — is the truthy check. Idiomatic Python writes if items: rather than if len(items) > 0:.
== vs isTwo of Python’s most-confused operators. == asks value equality (do they contain the same data?). is asks identity (are they the same object in memory?). They look similar but answer fundamentally different questions, and using the wrong one is a common source of subtle bugs.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
[a == b, a is b, a is c][True, False, True]
a == b — both lists contain the same items, so value equality says True.a is b — each [1, 2, 3] literal builds a fresh list, so they’re distinct objects in memory; identity says False.c = a makes c a second label on the same list, no copy. So a is c is True.The general rule: use == for value comparison. Use is only for the singletons — None, True, False:
result = None
result is NoneTrue
is None is correct, == None is poor style. The deep dive on identity, value, and aliasing is in Chapter 18.
Python’s primitives are few and consistent — but the trap is that == and is are different questions, and floats are not exact. Knowing which question you’re asking, and which comparison to use, prevents the most common kinds of bug.
This chapter introduces the primitives. The full data-model treatment of how == and is plug into special methods (__eq__, __hash__) is Chapter 13. The aliasing semantics — why a = b doesn’t copy — is Chapter 18.
Most data-analysis problems start with: “given some numbers, what’s their min, max, and mean?” We’ll build a summary(numbers) function that exercises the chapter’s primitives, arithmetic, and truthiness rules.
Step 1: the happy path. Take a list of numbers and return min/max/mean as a dict:
def summary(numbers):
return {
"n": len(numbers),
"min": min(numbers),
"max": max(numbers),
"mean": sum(numbers) / len(numbers),
}
summary([2, 4, 4, 4, 5, 5, 7, 9]){'n': 8, 'min': 2, 'max': 9, 'mean': 5.0}
min, max, sum, and len are built-in functions that work on any iterable. Note sum(numbers) / len(numbers) — true division (/) returns a float, which is what we want for a mean (5.0, not 5).
Step 2: handle the empty case with truthiness. Calling min([]) raises ValueError, and sum([]) / len([]) raises ZeroDivisionError. Use the empty-list-is-falsy rule for an early return:
def summary(numbers):
if not numbers:
return {"n": 0, "min": None, "max": None, "mean": None}
return {
"n": len(numbers),
"min": min(numbers),
"max": max(numbers),
"mean": sum(numbers) / len(numbers),
}
[summary([]), summary([3.5])][{'n': 0, 'min': None, 'max': None, 'mean': None},
{'n': 1, 'min': 3.5, 'max': 3.5, 'mean': 3.5}]
if not numbers: reads as “if the list is empty” — the idiomatic check, not if len(numbers) == 0:. We return None for the missing values since no min/max/mean exists for zero data.
Step 3: compare two summaries safely. Comparing summaries with == would compare their mean floats with ==, which we’ve seen is fragile. The right comparison uses math.isclose on the float fields:
import math
def same_summary(s1, s2):
if s1["n"] != s2["n"]:
return False
if s1["n"] == 0:
return True
return (s1["min"] == s2["min"]
and s1["max"] == s2["max"]
and math.isclose(s1["mean"], s2["mean"]))
a = summary([0.1, 0.2, 0.3])
b = summary([0.3, 0.2, 0.1])
[a == b, same_summary(a, b)][True, True]
a == b returns False because the means differ in the last few bits (0.20000000000000004 vs 0.2). same_summary correctly says True — same data, same answer.
The build exercises every concept from the chapter: primitive types (int, float, None), arithmetic (/ for mean), truthiness (if not numbers), and the ==-vs-is lesson generalized to floats (== is wrong for floats; math.isclose is right).
Float fragility. Predict 0.1 + 0.2 == 0.3. Confirm it’s False. Then use math.isclose to write the right comparison.
Floor division on negatives. Compute -7 // 2 and -7 % 2. Verify the identity (a // b) * b + (a % b) == a.
Truthy table. Write a one-line list comprehension that prints bool(x) for every value in [0, 0.0, "", [], (), {}, "0", 0.0001].
is vs ==. Set a = 257; b = 257; print(a is b). Now do the same with a = 5; b = 5. Why are some small integers cached?
Banker’s rounding. Predict the output of round(0.5), round(1.5), round(2.5). Look up “banker’s rounding” — why does Python round 2.5 to 2?
Five primitive types — int, float, bool, str, None — plus arithmetic, assignment, truthiness, and the ==/is distinction. That is the surface of the language. The next chapter, Chapter 3, takes one of those primitives — str — and develops the methods, slicing, and f-string formatting that make text manipulation in Python pleasant.