Literals, Numbers, and Bytes

Updated

September 8, 2026

Literals, Numbers, and Bytes

A literal is a value written in the source: 12, 1_850, 0xFF, "window", b"OK". The boring default is to write numbers so a human can read them, keep money in cents or Decimal, and treat bytes as raw and str as text.

Mental model

Python reads an integer in decimal unless you mark a base: 0x hex, 0o octal, 0b binary. Underscores in numbers are ignored: 1_850 is 1850. They exist so a price or a mask is skimmable.

int division with // stays an int. / always gives a float. float is binary floating point. 0.1 + 0.2 is not 0.3. For money, store integer cents or use decimal.Decimal with a string constructor.

A str is Unicode text. bytes is a sequence of 0–255. You move between them with encode and decode and an encoding name, almost always "utf-8". Adding a str to bytes raises TypeError.

Text formatting: an f-string (f"table {n}") is the boring default. str.format still works. % formatting is legacy.

Worked examples

Case 1: Readable numbers and bases

Save as readable_numbers.py.

# readable_numbers.py
def main():
    cents = 1_850
    seats = 10_000
    flags = 0xA5
    print(cents, seats, flags)
    print(hex(flags), bin(flags))
    print(12 / 5)
    print(12 // 5)
    print(12 % 5)


if __name__ == "__main__":
    main()

Run:

uv run python readable_numbers.py

Output:

1850 10000 165
0xa5 0b10100101
2.4
2
2

0xA5 is 165. / is float division even when both sides are ints. // is floor division. % is remainder.

Case 2: Float is the wrong type for a check

# float_check.py
from decimal import Decimal


def main():
    print(0.1 + 0.2)
    print(Decimal("0.1") + Decimal("0.2"))
    soup = 850
    tea = 400
    print("cents", soup + tea)
    print("dollars", (soup + tea) / 100)


if __name__ == "__main__":
    main()

Run:

uv run python float_check.py

Output:

0.30000000000000004
0.3
cents 1250
dollars 12.5

Add cents as ints. Convert to dollars only when you print. If you need exact decimal fractions, construct Decimal from a string, not from a float (Decimal(0.1) already has the error).

Case 3: str is text, bytes is raw

# label_bytes.py
def main():
    label = "café table 12"
    raw = label.encode("utf-8")
    print(type(label), label)
    print(type(raw), raw)
    print(list(raw))
    back = raw.decode("utf-8")
    print(back)
    print(label == back)


if __name__ == "__main__":
    main()

Run:

uv run python label_bytes.py

Output:

<class 'str'> café table 12
<class 'bytes'> b'caf\xc3\xa9 table 12'
[99, 97, 102, 195, 169, 32, 116, 97, 98, 108, 101, 32, 49, 50]
café table 12
True

é is two bytes in UTF-8 (195, 169). The b'...' display is Python’s way of showing those bytes. Decode with the same encoding you encoded with.

Case 4: f-strings versus format

# format_ticket.py
def main():
    table = 12
    cents = 1850
    print(f"table {table}: {cents} cents")
    print("table {}: {} cents".format(table, cents))
    print("table {t}: {c} cents".format(t=table, c=cents))
    print(f"table {table:>4}: ${cents / 100:.2f}")


if __name__ == "__main__":
    main()

Run:

uv run python format_ticket.py

Output:

table 12: 1850 cents
table 12: 1850 cents
table 12: 1850 cents
table   12: $18.50

Prefer the f-string. Use format specs (:>4, :.2f) inside the braces. Keep str.format when the template itself is a string you loaded from a file.

The trap

Mixing str and bytes, or using float as a running total for money, both fail in ways that look like “Python is weird” until you look at the types.

# mix_text.py
def main():
    header = b"TICKET"
    name = "12"
    print(header + name)


if __name__ == "__main__":
    main()

Run:

uv run python mix_text.py

Output (the process exits non-zero):

Traceback (most recent call last):
  File "mix_text.py", line 9, in <module>
    main()
  File "mix_text.py", line 5, in main
    print(header + name)
TypeError: can't concat str to bytes

The fix is one side: header + name.encode("utf-8") or header.decode("utf-8") + name. Pick text or raw and convert at the edge (socket, file, subprocess).

The boring rule

  • Write 1_850, not 1850, when the extra marks help a human.
  • Use 0x / 0b when the value is a bit pattern. Do not decorate ordinary counts.
  • Money: integer cents, or Decimal("18.50"). Never accumulate float dollars.
  • str in the program. bytes at the wire. Encode and decode with "utf-8" unless a protocol names another encoding.
  • f-strings for almost all formatting.

Try this

  1. In readable_numbers.py, print 0b1010 and 0o12 and confirm both are ten.
  2. Change float_check.py so a 10% service charge is applied in cents with integer arithmetic (cents * 10 // 100).
  3. In label_bytes.py, decode with "latin-1" instead of "utf-8" and print the result. Put "utf-8" back.