Three: int (whole numbers, unlimited size), float (double-precision
binary floating point), and complex (a real + imaginary part written with
j). bool is technically a subclass of int (True == 1).
a = 42 # int
b = 3.14 # float
c = 2 + 3j # complex
c.real, c.imag # (2.0, 3.0)
True + True # 2 — bool is an int subclass
Why it matters: mixing types promotes to the wider one (int + float -> float),
and knowing the three types explains conversion and precision behavior.
/ is true division and always returns a float. // is floor division
— it rounds toward negative infinity, not toward zero. % is the matching
modulo, and in Python its result takes the sign of the divisor.
7 / 2 # 3.5 — always float
7 // 2 # 3
-7 // 2 # -4 — floors toward -infinity, not -3
-7 % 2 # 1 — sign follows the divisor (2)
7 % -2 # -1 — sign follows the divisor (-2)
The identity always holds: (a // b) * b + (a % b) == a. Rule of thumb: Python's
floor/modulo differ from C/Java for negatives — expect non-negative % when the
divisor is positive.
Python int has arbitrary precision — it grows to hold any value, limited only
by available memory. There is no fixed 32/64-bit width, so computations never
silently wrap around like in C or Java.
2 ** 100 # 1267650600228229401496703205376
x = 10 ** 1000 # a 1001-digit integer — no overflow
import sys
sys.maxsize # largest "native" int, but ints can exceed it freely
Why it matters: you can compute huge factorials or cryptographic numbers directly, but very large ints cost more memory and arithmetic gets slower. Rule of thumb: integer overflow is simply not a concern in Python.
Floats are stored in binary (IEEE 754), and values like 0.1 and 0.2 have no exact binary representation — so tiny rounding errors accumulate. This is inherent to binary floating point, not a Python bug.
0.1 + 0.2 # 0.30000000000000004
0.1 + 0.2 == 0.3 # False
round(0.1 + 0.2, 2) # 0.3 — round for display
import math
math.isclose(0.1 + 0.2, 0.3) # True — tolerant comparison
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3') — exact
Rule of thumb: never compare floats with ==; use math.isclose or round, and
reach for Decimal when you need exact decimal arithmetic (e.g. money).
Bitwise operators work on the binary representation of integers: & (and),
| (or), ^ (xor), ~ (not/invert), << (left shift), and
>> (right shift). Shifting left by n multiplies by 2**n.
5 & 3 # 1 (0b101 & 0b011)
5 | 3 # 7 (0b111)
5 ^ 3 # 6 (0b110)
~5 # -6 (~x == -(x+1))
1 << 4 # 16 (1 * 2**4)
20 >> 2 # 5 (20 // 4)
Why it matters: bitwise ops power flags/bitmasks, fast power-of-two math, and
low-level protocols. Rule of thumb: ~x equals -(x + 1) because of two's
complement.
divmod(a, b) returns the quotient and remainder as a single tuple
(a // b, a % b) in one call. ** is exponentiation; with a third argument,
the built-in pow(base, exp, mod) does efficient modular exponentiation.
divmod(17, 5) # (3, 2) — quotient and remainder together
2 ** 10 # 1024
pow(2, 10) # 1024 — same as **
pow(2, 10, 1000) # 24 — (2**10) % 1000, computed efficiently
Rule of thumb: use divmod when you need both results (e.g. converting seconds to
minutes/seconds), and pow(a, b, m) for modular math instead of (a ** b) % m.
bool is a subclass of int, so True behaves as 1 and False as 0 in
any numeric context. This lets you sum booleans to count truthy items, but it can
also produce surprising results when bools sneak into math.
True + True # 2
sum([True, False, True]) # 2 — counts the Trues
isinstance(True, int) # True
["a", "b"][True] # "b" — True indexes as 1
Rule of thumb: sum(condition for x in data) is an idiomatic way to count matches,
but never rely on bool/int interchange where it harms readability.
int() truncates toward zero (drops the fractional part). round() does
banker's rounding (round-half-to-even). float() just widens to a float. They
are not interchangeable for negatives or .5 cases.
int(2.9) # 2 — truncates, no rounding
int(-2.9) # -2 — toward zero
round(2.5) # 2 — half to even
round(3.5) # 4 — half to even
round(2.675, 2) # 2.67 — float repr bites here
Rule of thumb: int() truncates, round() rounds-half-to-even; for predictable
decimal rounding use Decimal.
Python chains comparisons: a < b < c is evaluated as a < b and b < c, with
b evaluated once. Each operator is independent, so unusual chains are legal
(and sometimes confusing).
1 < 2 < 3 # True — like (1 < 2) and (2 < 3)
5 < 10 > 3 # True — legal but unusual
x = 5
0 <= x <= 10 # idiomatic range check
Rule of thumb: use chaining for readable range checks (lo <= x <= hi); avoid
mixing operator directions which obscures intent.
For immutable numbers, yes in effect — both rebind x to a new object (ints are
immutable, so += cannot mutate in place). The distinction matters for mutable
types, but numbers always produce a fresh object.
x = 5
id_before = id(x)
x += 1
id(x) == id_before # False — new int object
# contrast with a list, where += mutates in place:
lst = [1]; before = id(lst); lst += [2]; id(lst) == before # True
Rule of thumb: += on numbers/strings/tuples rebinds; on lists/sets/dicts it
mutates in place. The operator's meaning depends on the type's __iadd__.
Use math for functions operators don't provide and for correctness on floats:
math.sqrt, math.floor/ceil (return ints), math.isnan/isinf, math.gcd,
and math.isclose. Note math functions work on floats, not complex numbers.
import math
math.floor(-2.5) # -3 — returns int, toward -inf
math.ceil(2.1) # 3
math.gcd(12, 18) # 6
math.isclose(0.1 + 0.2, 0.3) # True
math.sqrt(2) # 1.4142135623730951
Rule of thumb: reach for math for floor/ceil-as-int, gcd, and float-safe checks;
use cmath when you need complex-number math.
nan ("not a number") is never equal to anything, including itself. This breaks
naive equality and makes containers behave oddly. Use math.isnan() to detect it.
nan = float('nan')
nan == nan # False
nan != nan # True — the canonical nan test
import math
math.isnan(nan) # True
nan in [nan] # True! — `in` uses identity-then-equality
sorted([3, nan, 1]) # unreliable order — nan breaks comparisons
Rule of thumb: detect nan with math.isnan, never == float('nan'), and scrub nans
before sorting or deduping.
CPython caches small integers from -5 to 256 as singletons, so identical
small ints share one object and is happens to return True. This is an
implementation detail — never use is to compare numeric values.
a = 256; b = 256
a is b # True — cached
a = 257; b = 257
a is b # False — not cached (in a script/REPL line)
a == b # True — always the right test
Rule of thumb: compare numbers with ==. is is for identity (e.g. is None),
and small-int caching is not something to rely on.
Write the imaginary part with a j suffix. Complex numbers support arithmetic and
have .real, .imag, .conjugate(), and abs() (the magnitude). Use the cmath
module for complex-aware math functions.
z = 3 + 4j
z.real, z.imag # (3.0, 4.0)
abs(z) # 5.0 — magnitude sqrt(3**2 + 4**2)
z.conjugate() # (3-4j)
import cmath
cmath.sqrt(-1) # 1j
Rule of thumb: use abs(z) for magnitude and cmath (not math) for roots/trig on
complex values.
Use f-string format specs: , for thousands separators, .2f for fixed
decimals, % for percentages, e for scientific, and b/o/x for binary/octal/
hex. The spec mini-language keeps formatting in one place.
n = 1234567.891
f"{n:,.2f}" # '1,234,567.89'
f"{0.0825:.1%}" # '8.2%'
f"{255:#x}" # '0xff'
f"{42:08b}" # '00101010' — zero-padded binary
f"{n:.2e}" # '1.23e+06'
Rule of thumb: format specs ({value:,.2f}) handle separators, padding, and bases
declaratively — avoid manual string surgery.
Underscores are digit group separators for readability — they're ignored by the parser. They work in int, float, and other-base literals, letting you write large constants clearly.
budget = 1_000_000 # same as 1000000
pi = 3.14_159
flags = 0b_1010_0001 # grouped binary
hex_color = 0xFF_FF_FF
Rule of thumb: use _ to group large literals (millions, byte/nibble boundaries);
it has zero effect on the value, only on readability.
More Fundamentals interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.