Characters and Numbers
There is nothing in a computer but ones and zeros. This lecture turns them back into things you recognize — letters, whole numbers, decimals — and explains why the decimals will lie to you.
By the end of this lecture you should be able to
- Read a complete short program and say what each line does
- Tell str, int, float and bool apart, and use type() to check a value
- Convert between binary, octal, decimal and hexadecimal
- Shape your output with escape sequences and triple-quoted strings
- Move between characters and their code points with ord() and chr()
- Explain why floating-point numbers are inexact, and know when to avoid them
- Predict correctly what type / // % and ** each return
Contents
Start with a whole program
Last lecture you wrote a single line, print("Hello world"). Here is a program that actually does something. Look at it whole first, then take it apart:
# Body mass index calculator
name = "Alex"
height = 1.75 # meters
weight = 68.5 # kilograms
bmi = weight / (height ** 2)
print(name, "has a BMI of", bmi)
print("to one decimal place:", round(bmi, 1))
print("above 24?", bmi > 24)
Run it, then read it line by line:
| Line | What it does | Covered in |
|---|---|---|
# Body mass index… | a comment — for humans; Python skips it | ✓ |
name = "Alex" | stores a string in a variable called name | ✓ characters |
height = 1.75 | stores a floating-point number | ✓ numbers |
bmi = weight / (height ** 2) | arithmetic, result kept for later | ✓ operators |
print(name, "has a BMI of", bmi) | output; several values separated by commas | ✓ output |
bmi > 24 | a comparison, giving True or False | ✓ logic |
Every program comes down to the same three moves: get some data, compute with it, hand the result back. This lecture is about what “data” actually is. L3 covers turning data into nicely formatted output, and L4 covers making the program decide and repeat.
The symbols in that line
Four symbols in bmi = weight / (height ** 2) have not been explained yet:
| Symbol | What it does |
|---|---|
= | assignment: put the value on the right into the name on the left. Mathematical equality is written with two equals signs in Python |
/ | divide |
** | power. height ** 2 is height squared |
() | do what is inside first, as in mathematics |
More operators show up later in this lecture. Here they all are at once — just get familiar with them; the sections on operators and on logic come back to each one.
Arithmetic operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ | add | 7 + 2 | 9 |
- | subtract | 7 - 2 | 5 |
* | multiply | 7 * 2 | 14 |
/ | divide | 7 / 2 | 3.5 |
// | floor division | 7 // 2 | 3 |
% | remainder | 7 % 2 | 1 |
** | power | 7 ** 2 | 49 |
Look at the / row: division always gives a float, so 4 / 2 is 2.0 and not 2. The section on operators explains why.
Comparison operators (the answer is always True or False)
| Operator | Meaning | Example | Result |
|---|---|---|---|
> | greater than | 7 > 2 | True |
< | less than | 7 < 2 | False |
>= | at least | 7 >= 7 | True |
<= | at most | 2 <= 7 | True |
== | equal to | 7 == 2 | False |
!= | not equal to | 7 != 2 | True |
Boolean operators (join several comparisons together)
| Operator | Meaning | Example | Result |
|---|---|---|---|
and | both must hold | True and False | False |
or | at least one holds | True or False | True |
not | flips true and false | not True | False |
How does it know which bits are text and which are a number
L1 ended on this: a computer holds nothing but 0s and 1s, and a bit pattern means nothing on its own. So how can the same eight bits, 01000001, be the letter A one moment and the integer 65 the next?
The answer is not inside those bits. It is in who reads them, and under which agreement. In a program, the thing that carries that agreement is the type.
Data types
Three different kinds of value already appear in that program: "Alex" is text, 1.75 is a number with a fractional part, and bmi > 24 comes out as true or false. The difference isn’t just that the values differ — they belong to different types.
Four types cover almost everything in the first half of this course:
| Type | Name | Examples | Section in this lecture |
|---|---|---|---|
| string | str | "Alex", "a", "" | characters |
| integer | int | 2026, -3, 0 | numbers |
| float | float | 1.75, -0.5, 3.0 | numbers |
| boolean | bool | True, False | logic |
type() tells you which one a value belongs to:
print(type("Alex"))
print(type(1.75))
print(type(2026))
print(type(1.75 > 1))
Don’t worry about the word class in the output — it’s there because types are themselves objects in Python, which is a Part IV story.
A type isn’t just a label. It decides what you can do with the value, and — this is the part that bites people — what a given operator actually means:
print(3 * 2)
print("ab" * 2)
print(1 + 2)
print("1" + "2")
* multiplies numbers but repeats strings; + adds numbers but glues strings together. Which means “what does this line do?” is an unanswerable question until you know the types of the operands. The type rules table later in this lecture nails down what each arithmetic operator returns, and L3 covers converting between types.
Where the type lives
“The type travels with the value” is not a metaphor. Write n = 2026 and this is what sits in memory:
nis only a name. It holds nothing itself; it points at a place- The object it points at is 28 bytes, in four parts:
| Bytes | What it holds |
|---|---|
| 0–7 | reference count — how many names point here |
| 8–15 | the type — a pointer to int |
| 16–23 | length and sign |
| 24–27 | the value itself; 2026 is here |
type(n) reads bytes 8–15. That answers the question from the previous section: the machine never has to guess — the value carries its own answer.
C takes the other road. In int n = 2026; the type is written before the name, and once compiled, memory holds only the value; “this is an int” lives in the instructions the compiler emitted. That saves the 20 bytes above, at the price of a type that cannot change.
Everything is bits
One bit, one byte
A storage cell has two states — high voltage and low voltage — which we write as 1 and 0.
- a single 0 or 1 is a bit
- eight bits make a byte
One bit distinguishes 2 things, two bits distinguish 4, and n bits distinguish 2ⁿ. So one byte can stand for 2⁸ = 256 different things.
Keep that number in mind. It comes up constantly.
Counting in binary
In decimal, 2026 means 2×10³ + 0×10² + 2×10¹ + 6×10⁰. Each step left multiplies the weight by 10.
Binary works identically, with weights multiplying by 2. So 1101 is:
1 1 0 1
×2³ ×2² ×2¹ ×2⁰
8 + 4 + 0 + 1 = 13
# Python will do the conversion for you
print(0b1101) # a binary literal, prefix 0b
print(bin(13)) # and back the other way
print(int("1101", 2)) # parse a string as base 2
Four ways to write the same integer
Python accepts integer literals in four bases:
| Base | Prefix | Example | Value |
|---|---|---|---|
| decimal | none | 96 | 96 |
| binary | 0b | 0b1100000 | 96 |
| octal | 0o | 0o140 | 96 |
| hexadecimal | 0x | 0x60 | 96 |
print(96, 0b1100000, 0o140, 0x60) # four spellings, one number
print(96 == 0x60) # confirm
These differ only on the page. Once stored, they are identical — much as “twelve”, “12” and “XII” all name the same quantity.
Hexadecimal is popular because one hex digit corresponds exactly to four binary digits, so it is compact while still showing the bit structure at a glance.
Characters
ASCII: an agreement between letters and numbers
A computer stores numbers. So what happens to letters?
You agree on a table that says which number stands for which character. The first such table to be widely adopted is ASCII — the American Standard Code for Information Interchange — which uses 7 bits for 128 characters: upper and lower case letters, digits, punctuation and some control codes.
The table splits into bands by purpose, and inside a band the codes run consecutively:
| Codes | What that band is |
|---|---|
| 0–31 | control characters, with no visible shape (Tab is 9, newline 10, return 13) |
| 32 | space |
| 33–47, 58–64, 91–96, 123–126 | punctuation, scattered in between, no pattern |
| 48–57 | the digits 0–9 |
| 65–90 | uppercase A–Z |
| 97–122 | lowercase a–z |
| 127 | DEL |
Being consecutive is what makes the bands useful: to ask whether a character is a digit, check whether its code falls between 48 and 57 — no need to compare against each one.
A few worth remembering:
| Character | Code | Note |
|---|---|---|
'0' | 48 | the digit character zero, not the integer 0 |
'A' | 65 | where the uppercase letters start |
'a' | 97 | lowercase = uppercase + 32 |
' ' | 32 | a space is a character too |
print(ord('A'), ord('a'), ord('0'), ord(' '))
print(chr(65), chr(97), chr(48))
# the 32 gap means you can convert case by arithmetic
print(chr(ord('h') - 32))
ord() gives you a character’s code point; chr() turns a code point back into a character. They are the bridge between characters and numbers.
128 slots is not enough for the world’s writing systems. The modern answer is Unicode, which assigns a code point to essentially every character in use. ord() works there too:
print(ord('中'), ord('文'))
print(chr(20013), chr(25991))
How Unicode actually works — and why text sometimes arrives as garbage — is L12’s business.
Escape sequences
Some characters cannot simply be typed between quotes: a newline, a tab, or a quote mark itself. The answer is an escape sequence starting with a backslash:
| Written | Means |
|---|---|
\n | newline |
\t | tab |
\\ | a single backslash |
\' | single quote |
\" | double quote |
print("first line\nsecond line")
print("name\tid")
print("She said \"hello\"")
print("path C:\\Users\\Documents")
Multi-line strings
Once a piece of text has more than a couple of line breaks, a line littered with \n
stops being readable. Wrap it in three quotes instead and the line breaks are kept
exactly as written:
poem = """The woods are lovely, dark and deep,
But I have promises to keep,
And miles to go before I sleep."""
print(poem)
There is a second benefit: single and double quotes can appear inside without escaping.
print("""He said "hello", and I said 'goodbye'.""")
A mental model for print()
Think of print() as a pen moving across paper. It writes left to right, and each call ends by moving to the start of the next line.
Several arguments separated by commas get a single space between them automatically:
print("a", "b", "c") # spaces appear between them
print("a" + "b" + "c") # concatenation — no spaces
print(1, 2, 3)
To suppress the line break, use end:
print("no break here", end="")
print(" ← same line")
print("separated", end=" --- ")
print("next piece")
Numbers
Integers have no upper limit in Python
In most languages an integer occupies a fixed width — 64 bits, say — and going past it overflows: the value silently wraps around, often turning negative.
Python does not do that. Integers grow as large as you like, bounded only by memory:
big = 2 ** 200
print(big)
print("that is", len(str(big)), "digits")
# factorials get out of hand faster
import math
print(math.factorial(50))
This is one of Python’s real kindnesses to beginners — overflow is simply not something you have to think about. The cost is that arithmetic on huge integers slows down, and L11 quantifies by how much.
Floating point, and why it lies
Decimals in Python are floating-point numbers (float). Here is an experiment. Predict the output before running it:
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
If you predicted 0.3 and True, congratulations — you have just met the trap that catches every programmer once.
Why does this happen?
Computers store fractions in binary. In decimal, 1/3 = 0.3333… never terminates. In binary, 0.1 never terminates either:
0.1 (decimal) = 0.000110011001100110011… (binary, 0011 repeating forever)
Storage is finite — usually 64 bits, following the IEEE 754 standard — so the expansion gets cut off. What actually goes into memory is not 0.1 but a number very close to 0.1. Add three such approximations together and the discrepancy surfaces.
# print more digits than usual to see what is really stored
print(f"{0.1:.20f}")
print(f"{0.2:.20f}")
print(f"{0.1 + 0.2:.20f}")
There it is: 0.1 is really 0.10000000000000000555…
Sometimes it works, though
Go and try a few more combinations and you will hit this fairly quickly:
print(0.1 + 0.2 == 0.3)
print(0.1 + 0.3 == 0.4)
print(0.2 + 0.4 == 0.6)
print(0.1 + 0.7 == 0.8)
The second one is True. Mathematically these are all the same kind of statement, yet they don’t agree.
It isn’t random. A truncation error has a direction — the stored value can land above or below the number you asked for, depending on where the binary expansion got cut off:
print(f"{0.1:.20f}") # a little too big
print(f"{0.3:.20f}") # a little too small
print(f"{0.1 + 0.3:.20f}")
print(f"{0.4:.20f}") # identical to the line above
0.1 is stored slightly high and 0.3 slightly low, so the two errors point in opposite directions and cancel. The sum lands exactly on the value stored for 0.4, and == says yes. With 0.1 and 0.2 both errors point the same way and add up: the sum is 0.30000000000000004441 while 0.3 is stored as 0.29999999999999998890 — different numbers, so == says no.
Any machine, any mainstream language, same answers.
None of which is a reason to use == — it is the reason not to. Accidental equality is real, but working out in advance whether two errors will cancel costs far more than just comparing with a tolerance. And there is a nastier problem: a test that holds for some inputs is more dangerous than one that never holds, because it sails through your test cases and then fails on somebody else’s data.
When to steer clear of floats
A practical rule: if integers will do, use integers.
Money is the classic case. Adding 0.1 and 0.2 dollars goes wrong, but working in cents — 10 + 20 = 30 — is exact forever:
# the problematic version
total = 0.0
for _ in range(10):
total += 0.1
print("adding 0.1 ten times:", total)
print("equal to 1.0?", total == 1.0)
# integers instead (units of cents)
total_cents = 0
for _ in range(10):
total_cents += 10
print("in cents:", total_cents / 100)
The first loop lands on 0.9999999999999999 — ten tiny errors accumulating.
Operators
Arithmetic
a, b = 7, 2
print(a + b) # add
print(a - b) # subtract
print(a * b) # multiply
print(a / b) # divide
print(a // b) # floor division
print(a % b) # remainder
print(a ** b) # power
The type rules — learn this table
Which operations produce an integer and which produce a float is entirely predictable:
| Operation | Rule | Example |
|---|---|---|
/ | always returns a float, even when it divides evenly | 4 / 2 → 2.0 |
// | int if both operands are ints, otherwise float | 7 // 2 → 3, 7.0 // 2 → 3.0 |
% | same as // | 7 % 2 → 1 |
+ - * | int if both are ints, otherwise float | 2 * 3 → 6, 2 * 3.0 → 6.0 |
** | same, except a negative exponent gives a float | 2 ** 3 → 8, 2 ** -1 → 0.5 |
print(4 / 2, type(4 / 2)) # note the 2.0
print(7 // 2, type(7 // 2))
print(7.0 // 2, type(7.0 // 2))
print(2 ** -1, type(2 ** -1))
Floor division and remainder with negatives
// rounds down — toward the smaller value — rather than chopping off the fractional part. For negative numbers those are not the same thing:
print(7 // 2) # 3
print(-7 // 2) # -3 or -4? guess first
print(7 % 2)
print(-7 % 2) # guess this one too
Python guarantees that a == (a // b) * b + a % b always holds, which forces -7 // 2 to be -4 and -7 % 2 to be 1.
Precedence
As in ordinary arithmetic: ** binds tightest, then * / // %, then + -. Equal precedence groups left to right — except **, which groups right to left.
print(2 + 3 * 4) # 14, not 20
print(2 ** 3 ** 2) # 2^(3^2) = 512, not (2^3)^2 = 64
print((2 + 3) * 4) # 20
Logic: True and False
What they are for
Arithmetic answers how much. A comparison answers yes or no — and that is what lets a program decide anything at all.
Every program in this lecture so far runs straight through, first line to last, taking the same path whatever the input. From L4 onward programs branch and repeat, and the condition after every if and every while must evaluate to True or False:
the program reaches this line
│
┌─────┴─────┐
│score >= 60│ ← this evaluates to True or False first
└─────┬─────┘
True │ False
┌─────┴─────┐
▼ ▼
print("pass") print("fail")
That is two steps, not one: evaluate score >= 60 into a boolean, then pick a path with it. It is tempting to think the program simply “looks at the score and knows” — which skips the value in the middle. Once conditions grow into score >= 60 and attendance >= 0.8, you cannot debug them without seeing that middle value.
The same shape shows up elsewhere: while guess != answer decides whether a loop stops, if name in roster asks whether something is there, and adding a row of comparisons counts how many of them hold (the last snippet in this section).
Comparisons
A comparison produces a boolean — one of exactly two values, True and False. Note the capital letters.
print(3 > 2)
print(3 == 2) # two equals signs means "is equal to"
print(3 != 2) # not equal
print(3 >= 3)
Booleans combine with and, or and not:
age = 19
has_id = True
print(age >= 18 and has_id)
print(age < 18 or has_id)
print(not has_id)
Underneath, True and False are 1 and 0, and they can go straight into arithmetic:
print(True + True) # 2
print(sum([True, False, True, True])) # how many are True
That last pattern is genuinely useful — counting how many items satisfy a condition comes up all the time.
Summary
- Every value has a type —
str,int,float,boolare the four you will use constantly. The type decides what you can do with the value, and what+or*even mean on it;type()tells you which one you have - There are only bits. All meaning comes from convention: the same string of bits can be a number, a character, or an instruction
- Four bases, one number:
0b,0o,0x, or no prefix ord()andchr()move between characters and code points.'0'(a character) and0(an integer) are different- Python integers have no upper limit — overflow is not your problem
- Floats are inexact, because binary cannot represent most decimal fractions. Never compare them with
==— a few (0.1 + 0.3 == 0.4) happen to come out true, but you cannot tell which ones in advance - If integers will do, use integers
/always returns a float;//rounds down, which matters for negatives=assigns,==compares
Exercises
- Without looking anything up, predict the value and the type of each:
9 / 3,9 // 3,9 % 3,9 ** 0.5,-9 // 3,-9 % 3. Then run them. Note down whichever you got wrong. - Write a program that takes an uppercase letter and prints the lowercase version, using
ord()andchr()rather than.lower(). - Print your student number in binary, octal and hexadecimal, then use
int()to convert all three back to decimal and check they agree. - Demonstrate floating-point imprecision: find three pairs of numbers whose sum in Python differs from the mathematically correct answer (
0.1 + 0.2being one), and print each to 20 decimal places. - Using
print()’sendargument, output1 2 3 4 5on one line separated by->, with no trailing arrow. - Run
2 ** 2000, then run2.0 ** 2000. The first gives you a 603-digit integer; the second raisesOverflowError. Explain why. Then find the largest integernfor which2.0 ** nstill works, and account for that particular number.
The lab problems for this lecture are in Lab 2.