Python · Python Essentials

Python: Numbers, Bits and Strings

Ints that never overflow, // and % rounding down, bit tricks without a sign bit, float traps; str immutability and join, code points, split and int().

  • 10 min read
  • 20 practice questions
  • 2 graded problems
On this page
  1. Description
  2. Integers that never overflow
  3. Division and rounding
  4. Working with bits
  5. Floats
  6. Strings never change
  7. Characters and emoji
  8. The string methods that surprise people
  9. Which tool for which job?
  10. Common mistakes
  11. Check yourself
  12. Key takeaways
  13. Quick revise
  14. Practice

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:

  1. Integers that never overflow - and what they cost instead.
  2. Division and remainder, including negative numbers and rounding.
  3. Working with bits when numbers have no fixed width.
  4. Floats: the same as everywhere else, and how to live with them.
  5. Strings: why they never change, and how to build them efficiently.
  6. 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!.
  • bool is a kind of int. True + True is 2, sum(flags) counts the true ones, and True, 1 and 1.0 are 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 -1 is 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") raises ValueError - int() reads whole numbers only. Use int(float(s)) only if you really mean to accept decimals.
  • "²".isdigit() is True, yet int("²") raises ValueError. isdigit() accepts superscripts and other digit-like symbols; isdecimal() is the safe check before int().

Which tool for which job?

You wantUseNot
An index or midpoint/// (gives a float)
A remainder that's never negative% (positive divisor)-
Java-style truncating division on huge intstrunc_div with // and signsint(a / b) (goes through a float)
Rounding halves updecimal with ROUND_HALF_UPround() (rounds halves to even)
32-bit bit tricksmask with & 0xFFFFFFFF, fix the sign at the endraw loops on negative ints
Exact moneyinteger cents or Decimalfloat
Build a string in a loopa list, then "".join(parts)s += piece
Split into wordssplit()split(" ")
Remove a suffixremovesuffix(".txt")rstrip(".txt")
Check before int()isdecimal()isdigit()

Common mistakes

MistakeWhat happensDo this instead
Bit loop on a negative numbernever endsmask with & 0xFFFFFFFF first
32-bit answer built bit by bit-3 comes out as 4294967293subtract 1 << 32 when the top bit is set
mid = (lo + hi) / 2float index, TypeError//
int(a / b) on huge intswrong past 2^53exact integer division with signs
No % MOD in a growing DPright answer, but times outreduce at every step
round(2.5) expecting 3gives 2Decimal with ROUND_HALF_UP
s[0] = "H"TypeErrorlist, edit, join
rstrip(".txt")strips characters, not a suffixremovesuffix
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, and round sends 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, removesuffix for suffixes, isdecimal() before int().

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You wantUseNot
An index or midpoint/// (always a float)
A remainder that's never negative% with a positive divisor-
Java-style truncating division on huge intsexact // on absolute values, then fix the signint(a / b) (float, wrong past 2^53)
Halves rounded upDecimal(...).quantize(..., ROUND_HALF_UP)round() (halves to even)
32-bit bit tricksn &= 0xFFFFFFFF, sign fix at the endraw loops on negative ints
Count set bitsn.bit_count() (of abs(n))a while n loop on negatives
Exact money / ratiosinteger cents, Decimal, Fractionfloat
Build a stringlist + "".join(parts)s += piece
Words from textsplit()split(" ")
Remove a suffixremovesuffix(".txt")rstrip(".txt")
Check before int()isdecimal()isdigit()
Case-insensitive comparecasefold() (+ NFC normalize)lower()

Snippets worth knowing by heart

-7 // 2, -7 % 2, divmod(-7, 2)       # -4, 1, (-4, 1)
-(-a // b)                           # ceiling division
(i - 1) % n                          # step back around a ring
n & 0xFFFFFFFF                       # low 32 bits
x - (1 << 32) if x >= 1 << 31 else x # 32-bit pattern -> signed
x & -x                               # lowest set bit
pow(base, exp, MOD)                  # fast modular power
math.isqrt(n)                        # exact integer square root
float("inf")                         # infinity sentinel
unicodedata.normalize("NFC", s)      # compare text safely

The traps

  • Bit loops on negative ints never end - a negative int has infinitely many leading 1s.
  • A 32-bit answer built bit by bit comes out as 4294967293 instead of -3 without the sign fix.
  • / for an index - TypeError: list indices must be integers or slices, not float.
  • No % MOD in a DP - correct but slow as ints grow.
  • round(2.5) is 2; round(2.675, 2) is 2.67.
  • Printing an int over 4,300 digits raises ValueError (3.11+).
  • True, 1, 1.0 are one dict key.
  • s[0] = "H" - strings are immutable.
  • "a,b,,".split(",") keeps trailing empty pieces; rstrip takes a character set; "²".isdigit() is True.
  • is on numbers or strings - works by accident for small ints and interned strings.

Staff-level points, one line each

  • Ints are arrays of 30-bit digits: 28 bytes for 0 and 1, 32 at 2^30, 36 at 2^60; Karatsuba multiplication above a size threshold.
  • The 4,300-digit limit fixes CVE-2020-10735 (quadratic int ↔ decimal string); hex/binary conversions aren't limited; sys.set_int_max_str_digits(0) lifts it.
  • Ints -5..256 are shared (immortal since 3.12); identifier-like strings are interned - which is why is "sometimes works".
  • repr(0.1) is the shortest round-trip string; Decimal(0.1) shows the stored value 0.1000000000000000055511151231257827...
  • sum() of floats is compensated since 3.12 (sum([0.1]*10) is 1.0); math.fsum is exactly rounded on every version.
  • Decimal defaults to 28 significant digits; build it from strings, never floats.
  • PEP 393: a string uses 1, 2 or 4 bytes per code point by its widest character - 100 ASCII chars 141 B, add one emoji → 460 B. Hashes are cached per string.
  • += on strings is in-place only while nothing else references the string, and only in CPython.
  • Numbers hash by value mod 2^61 − 1 (hash(-1) == hash(-2) == -2); string hashes are randomized per process (PYTHONHASHSEED).
  • Unicode: "é" != "é" until NFC-normalized; "ß".casefold() is "ss"; grapheme clusters need the third-party regex module's \X.

Say it in an interview

  • "Python ints don't overflow, so I'll still reduce modulo every step to keep them small and fast."
  • "The problem is 32-bit, so I mask with 0xFFFFFFFF and convert back to signed at the end."
  • "I use // for the midpoint - / would give a float index."
  • "I build the output in a list and join it once."

Practice

This skill comes with 20 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python: Numbers, Bits and Strings free

Mirror a 32-bit Register

MediumProCoding problemPython

A firmware tool in Austin talks to a sensor chip whose register stores a value as a signed 32-bit integer, but the chip's bus sends the 32 bits in the opposite order.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Divide Like the Ledger

MediumProCoding problemPython

A bank in Dublin is moving its ledger service from Java to Python. The Java code divided amounts (in cents, which can be very large) with BigInteger.divide and remainder: the quotient is rounded toward zero, and the remainder has the sign of the dividend, so that quotient * b +…

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Python: Numbers, Bits and Strings free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime