CS1602Introduction to Computation
Lecture 2Part 1 Foundations of ComputationAI Level 0

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:

LineWhat it doesCovered in
# Body mass index…a comment — for humans; Python skips it✓
name = "Alex"stores a string in a variable called name✓ characters
height = 1.75stores 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 > 24a 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:

SymbolWhat 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

OperatorMeaningExampleResult
+add7 + 29
-subtract7 - 25
*multiply7 * 214
/divide7 / 23.5
//floor division7 // 23
%remainder7 % 21
**power7 ** 249

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)

OperatorMeaningExampleResult
>greater than7 > 2True
<less than7 < 2False
>=at least7 >= 7True
<=at most2 <= 7True
==equal to7 == 2False
!=not equal to7 != 2True

Boolean operators (join several comparisons together)

OperatorMeaningExampleResult
andboth must holdTrue and FalseFalse
orat least one holdsTrue or FalseTrue
notflips true and falsenot TrueFalse

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:

TypeNameExamplesSection in this lecture
stringstr"Alex", "a", ""characters
integerint2026, -3, 0numbers
floatfloat1.75, -0.5, 3.0numbers
booleanboolTrue, Falselogic

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:

  • n is only a name. It holds nothing itself; it points at a place
  • The object it points at is 28 bytes, in four parts:
BytesWhat it holds
0–7reference count — how many names point here
8–15the type — a pointer to int
16–23length and sign
24–27the 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:

BasePrefixExampleValue
decimalnone9696
binary0b0b110000096
octal0o0o14096
hexadecimal0x0x6096
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:

CodesWhat that band is
0–31control characters, with no visible shape (Tab is 9, newline 10, return 13)
32space
33–47, 58–64, 91–96, 123–126punctuation, scattered in between, no pattern
48–57the digits 0–9
65–90uppercase A–Z
97–122lowercase a–z
127DEL

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:

CharacterCodeNote
'0'48the digit character zero, not the integer 0
'A'65where the uppercase letters start
'a'97lowercase = uppercase + 32
' '32a 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:

WrittenMeans
\nnewline
\ttab
\\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:

OperationRuleExample
/always returns a float, even when it divides evenly4 / 2 → 2.0
//int if both operands are ints, otherwise float7 // 2 → 3, 7.0 // 2 → 3.0
%same as //7 % 2 → 1
+ - *int if both are ints, otherwise float2 * 3 → 6, 2 * 3.0 → 6.0
**same, except a negative exponent gives a float2 ** 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, bool are 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() and chr() move between characters and code points. '0' (a character) and 0 (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

  1. 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.
  2. Write a program that takes an uppercase letter and prints the lowercase version, using ord() and chr() rather than .lower().
  3. Print your student number in binary, octal and hexadecimal, then use int() to convert all three back to decimal and check they agree.
  4. Demonstrate floating-point imprecision: find three pairs of numbers whose sum in Python differs from the mathematically correct answer (0.1 + 0.2 being one), and print each to 20 decimal places.
  5. Using print()’s end argument, output 1 2 3 4 5 on one line separated by ->, with no trailing arrow.
  6. Run 2 ** 2000, then run 2.0 ** 2000. The first gives you a 603-digit integer; the second raises OverflowError. Explain why. Then find the largest integer n for which 2.0 ** n still works, and account for that particular number.

The lab problems for this lecture are in Lab 2.