On this page
Description
Numbers and text are the raw material of nearly every coding problem: counting, summing, indexing into lists, parsing input, building output. Python makes them friendlier than most languages - its integers never overflow, % gives the answer you'd expect for negative numbers, and a string's length counts real characters. But a few of its rules are different from what you'd guess, especially if you've written Java or C before, and they're behind a lot of "right algorithm, wrong answer" moments in interviews.
Here's a small example. This function counts the 1 bits in a number, a classic interview warm-up:
def count_bits(n: int) -> int:
count = 0
while n:
n &= n - 1 # clear the lowest 1 bit
count += 1
return count
count_bits(5) returns 2, as it should. count_bits(-1) never returns at all. By the end of the "bits" section you'll know exactly why, and the one-line fix.
Here's the path, from easiest to most technical:
- Integers that never overflow - and what they cost instead.
- Division and remainder, including negative numbers and rounding.
- Working with bits when numbers have no fixed width.
- Floats: the same as everywhere else, and how to live with them.
- Strings: why they never change, and how to build them efficiently.
- Characters and emoji, and the string methods that surprise people.
Integers that never overflow
A Python int can be as big as your memory allows. There's no MAX_VALUE and no wrapping around:
2**31 - 1 + 1 # 2147483648: no wraparound
2**64 * 2**64 # 340282366920938463463374607431768211456, exact
pow(3, 10**18, 10**9 + 7) # 246336683: fast modular power, even with a huge exponent
math.isqrt(10**20 + 12345) # 10000000000: an exact integer square root
sys.maxsize (9223372036854775807 on a 64-bit machine) is the largest size a list can have, not a limit on integers.
So what replaces overflow bugs? Time. Arithmetic on a big integer takes longer the more digits it has. A dynamic-programming solution that keeps multiplying without % MOD still gives the right answer - eventually - while its numbers grow to thousands of digits and the solution times out. When a problem says "return the answer modulo 10^9 + 7", reduce at every step, just as you would in Java.
Two more things worth knowing:
- Printing huge numbers has a limit. Since Python 3.11, turning an int with more than 4,300 digits into a string raises
ValueError. It's a guard against slow conversions on hostile input.sys.set_int_max_str_digits(0)lifts it if you really need to print 20,000!. boolis a kind ofint.True + Trueis 2,sum(flags)counts the true ones, andTrue,1and1.0are the very same dictionary key:{True: "a", 1: "b", 1.0: "c"}ends up as{True: 'c'}.
Division and rounding
Python has two division operators, and the difference matters as soon as you compute an index:
7 / 2 # 3.5: "/" always gives a float
7 // 2 # 3: "//" gives a whole number, rounded down
(0 + 5) / 2 # 2.5 - can't be used as a list index
Using / for a midpoint, mid = (lo + hi) / 2, gives a float, and nums[mid] fails with TypeError: list indices must be integers or slices, not float. Use //.
With negative numbers, Python rounds down (toward minus infinity), and the result of % takes the sign of the number on the right:
-7 // 2 # -4: rounded down, not toward zero
-7 % 2 # 1: never negative when the divisor is positive
divmod(-7, 2) # (-4, 1): both at once
-(-7 // 2) # 4: ceiling division, without floats
int(-7 / 2) # -3: int() cuts toward zero, like Java's "/"
This makes two classic Java bugs impossible. Walking backwards around a circle of n items, (i - 1) % n is n - 1 when i is 0. And n % 2 == 1 is true for negative odd numbers too.
There's a reverse trap: some problems are defined with Java's or C's division, which cuts toward zero. int(a / b) gives that - but it goes through a float, and floats can't hold every integer above 2^53. With huge numbers, do it in exact integers:
def trunc_div(a: int, b: int) -> int:
q = abs(a) // abs(b)
return q if (a < 0) == (b < 0) else -q
Rounding. round() rounds halves to the nearest even number, so round(2.5) is 2 and round(3.5) is 4. This "banker's rounding" avoids a bias when you round many values. If a problem wants halves rounded up, say so explicitly: math.floor(x + 0.5), or the decimal module with ROUND_HALF_UP.
Working with bits
The bit operators are the familiar ones: &, |, ^, ~, <<, >>. What's different is that a Python int has no fixed width:
bin(5), bin(-5) # ('0b101', '-0b101'): a sign and a size, not 32 bits
-5 >> 1 # -3: ">>" rounds down, like // 2
~5 # -6: ~x is always -x - 1
-1 & 0xFFFFFFFF # 4294967295: the low 32 bits, read as an unsigned number
(-1).bit_count() # 1: bit_count() counts the 1 bits of abs(n)
A negative number behaves as if it had infinitely many 1 bits on its left. That's the answer to the puzzle at the top: count_bits(-1) keeps clearing the lowest 1 bit, and there's always another one. Python has no >>> operator either, because there's no "top bit" to fill.
When a problem talks about 32-bit integers, make the width explicit yourself:
- Before looping over bits, mask to 32 bits:
n &= 0xFFFFFFFF. Now-1is 32 ones, and the loop ends after 32 steps. - When you build a 32-bit answer bit by bit, turn it back into a signed number at the end:
x - (1 << 32) if x >= 1 << 31 else x. Without that step, an answer of -3 comes out as 4294967293.
One precedence rule to remember: &, | and ^ bind tighter than comparisons in Python, so x & 1 == 0 means (x & 1) == 0 - the opposite of C and Java. Parentheses still make it easier to read.
Floats
A Python float is the same 64-bit floating-point number as Java's double, with the same surprises:
0.1 + 0.2 == 0.3 # False
math.isclose(0.1 + 0.2, 0.3) # True: compare with a tolerance
2**53 + 1 == float(2**53 + 1) # False: the float can't hold that exact integer
float("inf") + 5 # inf: a handy "infinity" that stays infinite
Keep money in whole cents or decimal.Decimal, and use fractions.Fraction when you need exact ratios. float("inf") (or math.inf) is the natural "infinity" for shortest-path and minimum problems: adding to it and comparing it with ints both work.
Read more - comparing ints and floats is exact
When Python compares an int with a float, it compares the exact values, without first turning the int into a float. That's why 2**53 + 1 == float(2**53 + 1) is False: the float holds 2^53 exactly and the int is one more. Languages that convert first would say True.
Strings never change
A Python str is immutable: once created, it can't change. There's no separate character type either - indexing a string gives you another, one-character string:
s = "hello"
s[0] # 'h'
s[0] = "H" # TypeError: 'str' object does not support item assignment
s.upper() # 'HELLO': a new string; s is unchanged
To change characters, turn the string into a list, edit it, and join it back: chars = list(s); chars[0] = "H"; "".join(chars).
Building strings. "".join(parts) works out the total length once and copies each piece once, so it's always linear. s += piece in a loop creates a new string each time in principle. CPython can sometimes extend the string in place instead, when nothing else refers to it, so the same loop can be fast in one place and quadratic in another (the deep dive measures it). In an interview, collect the pieces in a list and join them at the end - it's never slow.
Characters and emoji
Python strings are sequences of code points - entries in the Unicode catalogue - so an emoji counts as one character, where Java counts two:
len("👍") # 1
len("👍🏽") # 2: a thumbs-up plus a skin-tone modifier
"a👍"[::-1] # '👍a'
ord("a"), chr(98) # (97, 'b')
s[::-1] reverses code points, which is right for most text. It goes wrong only for symbols built from several code points - skin tones, flags, letters written with a separate accent mark - which the deep dive covers.
The string methods that surprise people
" a b ".split() # ['a', 'b']: splits on runs of whitespace, no empty pieces
" a b ".split(" ") # ['', '', 'a', '', 'b', '']: every single space splits
"a,b,,".split(",") # ['a', 'b', '', '']: trailing empty pieces are kept
"report.txt".rstrip(".txt") # 'repor': strips any of the characters ".", "t", "x"
"report.txt".removesuffix(".txt") # 'report': removes the exact suffix
int(" 42\n"), int("1_000") # (42, 1000): spaces and underscores are allowed
"abc".find("z") # -1, while "abc".index("z") raises ValueError
Two more:
int("4.0")raisesValueError-int()reads whole numbers only. Useint(float(s))only if you really mean to accept decimals."²".isdigit()isTrue, yetint("²")raisesValueError.isdigit()accepts superscripts and other digit-like symbols;isdecimal()is the safe check beforeint().
Which tool for which job?
| You want | Use | Not |
|---|---|---|
| An index or midpoint | // | / (gives a float) |
| A remainder that's never negative | % (positive divisor) | - |
| Java-style truncating division on huge ints | trunc_div with // and signs | int(a / b) (goes through a float) |
| Rounding halves up | decimal with ROUND_HALF_UP | round() (rounds halves to even) |
| 32-bit bit tricks | mask with & 0xFFFFFFFF, fix the sign at the end | raw loops on negative ints |
| Exact money | integer cents or Decimal | float |
| Build a string in a loop | a list, then "".join(parts) | s += piece |
| Split into words | split() | split(" ") |
| Remove a suffix | removesuffix(".txt") | rstrip(".txt") |
Check before int() | isdecimal() | isdigit() |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Bit loop on a negative number | never ends | mask with & 0xFFFFFFFF first |
| 32-bit answer built bit by bit | -3 comes out as 4294967293 | subtract 1 << 32 when the top bit is set |
mid = (lo + hi) / 2 | float index, TypeError | // |
int(a / b) on huge ints | wrong past 2^53 | exact integer division with signs |
No % MOD in a growing DP | right answer, but times out | reduce at every step |
round(2.5) expecting 3 | gives 2 | Decimal with ROUND_HALF_UP |
s[0] = "H" | TypeError | list, edit, join |
rstrip(".txt") | strips characters, not a suffix | removesuffix |
isdigit() before int() | ValueError on "²" | isdecimal() |
Check yourself
Try to answer before opening each one. Recalling beats re-reading.
1. Why does while n: n &= n - 1 never end for n = -1, and what's the fix?
Show answer
A negative Python int behaves as if it had infinitely many 1 bits on its left, so clearing the lowest one never reaches zero. Mask to the width the problem means first (n &= 0xFFFFFFFF), or just use n.bit_count() when you want the count for the absolute value.
2. What are -7 // 2, -7 % 2 and int(-7 / 2)?
Show answer
-4, 1 and -3. // rounds down and % takes the divisor's sign; int() cuts toward zero, like Java's / - but going through a float is only exact for numbers up to 2^53.
3. What does "a,b,,".split(",") return?
Show answer
['a', 'b', '', '']. Python keeps empty pieces at the end (Java's split drops them).
4. A DP multiplies counts without ever taking % (10**9 + 7). The answer printed is correct, but the solution times out. Why?
Show answer
Python ints never overflow, so the numbers kept growing to thousands of digits, and each operation on them gets slower as they grow. Reduce modulo at every step.
Key takeaways
- Ints never overflow, but big ones cost time - reduce modulo as you go - and can't be printed past 4,300 digits by default.
/gives a float,//rounds down,%follows the divisor's sign, androundsends halves to the even neighbour.- Negative ints have no top bit: mask to 32 bits and fix the sign yourself.
- Floats are the same doubles as everywhere;
float("inf")is a safe "infinity". - Strings are immutable sequences of code points; build them with
"".join. split()for words,removesuffixfor suffixes,isdecimal()beforeint().