Strings, Variables and I/O
Programs have to deal with people — take something in, hand something back. This lecture also introduces functions, your first real weapon against complexity.
By the end of this lecture you should be able to
- Say precisely what an assignment does in memory, contrast C's variables with Python's, and explain id() and is with the name-points-at-object model
- Embed values in output with f-strings, controlling decimals and alignment
- Handle what input() gives back, and know what happens when conversion fails
- Define functions that take arguments and return values
- Keep print() and return straight, and stop confusing the two
Contents
Variables
What assignment does
x = 10
print(x)
x = 20 # same name, now bound to a new value
print(x)
Read = as “bind the value on the right to the name on the left.” It is not the equals sign of mathematics. In maths x = x + 1 is a false statement; in a program it is perfectly ordinary and means “take what x is now, add one, bind it back to x.”
count = 5
count = count + 1 # the right side evaluates to 6, then binds to count
print(count)
Naming
A name starts with a letter or underscore, and continues with letters, digits or underscores. Names are case sensitive — age and Age are different variables.
Python’s keywords are off limits (if, for, class, return and the rest):
import keyword
print(keyword.kwlist)
Convention matters more than the rules:
- Use meaningful names.
student_countbeatsn, andnbeatsx - Join words with underscores:
max_score,user_name. This is snake_case, and it is the Python convention - Avoid
l(lowercase L) andO(uppercase o) as standalone names — they look far too much like1and0
What a variable really is
Here are the same two lines in C and in Python:
int x = 10;
x = 20;
x = 10
x = 20
You won’t learn C in this course. All you need from those two lines: C makes you state a variable’s type before you use it (int means integer), and every statement ends with a semicolon.
Both end with x equal to 20. But when x = 20 runs, what happens to the 10? The two languages give different answers.
Memory is a long row of bytes, and each byte has a number — its address. A program finds data by its address.
In C, a variable is a piece of memory. int x = 10 sets aside 4 bytes, calls them x, and stores 10 in them. x = 20 overwrites those same 4 bytes with 20; the 10 is gone. That’s the box picture, and for C it’s accurate.
In Python, a variable is a reference. x = 10 does two things: it creates an integer object 10 somewhere in memory, and it makes x hold that object’s address. x holds an address, not the 10 itself. A name that holds an address and so points at an object is called a reference. x = 20 creates a new object 20 and points x at it. The 10 isn’t overwritten; x just no longer points at it.
C can point at things too, but only through a special feature called pointers, which this course doesn’t cover. In Python every name is a reference, with no extra syntax.
The box picture gives the right answers as long as you only have numbers and strings, which is why so many people keep using it for Python. It falls apart in L5, when lists arrive. Start with the arrow — name → object — and L5 will make sense.
id() gives you the address of the object a name currently points at:
x = 10
print(id(x))
y = x # y now points at the same object
print(id(y))
print(x is y) # is asks "the same one?"
After y = x the ids match: assignment copied the address, not the object. There is one 10 in memory and two names pointing at it.
The type lives on the object
If x holds nothing but an address, how does Python know it’s an integer and not a string?
L2 drew n = 2026 in memory: a 28-byte object whose bytes 8–15 record its type. The object carries its own type. type(x) follows the address in x to the object and reads that field.
That’s why Python has no type declarations, and why one name can point at objects of different types over time:
x = 2026
print(type(x))
x = "2026" # same name, now pointing at a str
print(type(x))
C works the other way round. Those 4 bytes hold the value and nothing else — nothing in memory says “this is an integer”. The type exists only in the source code, in the int of int x, and the compiler uses it to decide how to read the bytes. So a C variable must be declared with a type, and the type can’t change afterwards.
Immutable objects are safe to share
After y = x, both names point at the same 10. So if you change x, does y change too?
x = 10
y = x # y and x point at the same 10
x = x + 1 # makes a new 11, then points x at it
print(x, y)
print(x is y)
No. An integer object can’t be changed once it exists — it’s immutable. Strings are immutable too; you’ll see that in the next section. x = x + 1 doesn’t turn the 10 into 11. It creates a new 11 and points x at it, while y stays on the original 10.
Because nothing can change them, Python can let any number of names share one object without copying it. CPython goes further and creates the small integers from -5 to 256 in advance, so every 10 in your program is the same object.
Lists, in L5, can be changed in place. Once two names share a list, changing it through one name changes what the other sees.
== and is are different questions
== asks “are the values equal?”; is asks “are these the same object?”:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # same contents
print(a is b) # but not the same object
(The square brackets make a list, which is L5’s topic. It’s borrowed here because small integers are shared, so is on numbers doesn’t give a dependable answer.)
Same contents, so == is True. But they were built separately and occupy two places in memory, so is is False — two identical photocopies are still two sheets of paper.
What each design buys, and what it costs
| C | Python | |
|---|---|---|
| Storing one integer | 4 bytes | 8-byte address + 28-byte object |
| Reading it | read the 4 bytes directly | read the address, then find the object |
| Where the type lives | on the variable: must be declared | on the object: no declaration |
| Assigning a large object | copies the whole thing | copies an 8-byte address |
| What to watch for | types are fixed | several names sharing one mutable object |
| Designed for | staying close to the hardware; speed | uniform rules; flexibility |
(Byte counts are for CPython on a 64-bit machine.)
For a single number, Python uses several times the space and runs slower. What it gets in return: the type travels with the object, so names need no declarations; everything is handled the same way, so assigning a million-element list costs no more than assigning an integer — one address either way; and even functions can be assigned to names, as you’ll see later in this lecture.
C was designed to stay close to the hardware and run fast, which is what people writing operating systems and embedded software need. Python was designed to be easy to write: computers are fast now, most programs can afford to be a little slower, and the programmer’s time is worth more.
The cost is that several names can share one object. With immutable integers and strings that never causes trouble. With L5’s lists it produces some genuinely surprising behaviour — most of that lecture follows directly from this model.
Assigning several names at once
Both sides of = can be comma-separated, as long as the counts match:
a, b = 7, 3
print(a, b)
# the right side is evaluated in full first,
# so swapping needs no temporary variable
a, b = b, a
print(a, b)
The order is the point: the whole right-hand side is evaluated, and only then are the names bound. In a, b = b, a, the right side grabs the current b and a (3 and 7) before anything is rebound, and the swap falls out for free.
C and Java need a temporary variable for this. The pattern shows up again and again — catching several return values from a function, and pairing with enumerate in L4’s loops.
Compound assignment
x = 10
x += 3 # same as x = x + 3
print(x)
x -= 5
x *= 2
x //= 3
print(x)
+=, -=, *=, /=, //=, %= and **= all work this way. Shorter, and the name appears only once — so when you rename it, there is nothing to miss.
Comments
Everything after a # to the end of the line is ignored by Python and written for humans.
# this whole line is a comment
x = 3.14 # and this part is too
print(x)
Good comments explain why, rather than restating what the code plainly does:
# poor: add one to x
x += 1
# better: skip the header row
x += 1
Strings
Three kinds of quotes
a = 'single'
b = "double"
c = """triple quotes
span
multiple lines"""
print(a, b)
print(c)
Single and double quotes are entirely equivalent. One practical difference: whichever kind appears inside your text, use the other one outside, and you avoid escaping altogether.
print("He said 'hello'")
print('She said "hello"')
Joining and repeating
first = "Shanghai"
second = "Jiao Tong"
print(first + " " + second) # + joins
print("-" * 30) # * repeats
print(first * 2)
Length, indexing and comparison
s = "Python"
print(len(s)) # length
print(s[0]) # first character; indexing starts at 0
print(s[-1]) # last
print(s[2:5]) # a slice: includes the start, stops before the end
Strings compare lexicographically, character by character, using their code points:
print("apple" < "banana") # 'a' < 'b'
print("Zebra" < "apple") # uppercase code points are smaller
print("abc" < "abd") # first two match, decide on the third
print("abc" < "abcd") # a prefix sorts first
Strings are immutable — you cannot change a character in place:
s = "hello"
s = s.upper() # this builds a new string and rebinds s
print(s)
s = "hello"
s[0] = "H" # this raises an error
String methods get proper treatment in L6. This is enough for now.
Converting between types
type() tells you what something is:
print(type(42))
print(type(3.14))
print(type("42"))
print(type(True))
int(), float() and str() convert:
print(int("42") + 1) # string → integer
print(float("3.14") * 2) # string → float
print(str(42) + "!") # number → string
print(int(3.99)) # float → integer: truncates, does not round
print(int(-3.99)) # negatives truncate toward zero as well
Conversion raises an exception when it cannot succeed:
print(int("abc"))
print(int("3.14")) # this fails too — see if you can say why
int() accepts only a string that spells an integer. "3.14" has to go through float() first.
Input
input() always returns a string
# clicking Run opens a prompt box — try typing a number
age = input("How old are you? ")
print("you entered:", age)
print("its type is:", type(age))
Whatever you type, input() hands back a string. Type 18 and you get "18", not 18.
This is the single most common mistake of the first month:
n = input("Enter a number: ")
print(n * 3) # you wanted triple; you got it repeated three times
Convert first:
n = int(input("Enter a number: "))
print(n * 3)
eval(): useful, and worth being careful with
eval() evaluates a string as a Python expression:
print(eval("1 + 2 * 3"))
print(eval("2 ** 10"))
Convenient — eval(input()) reads a number or a whole expression in one go.
Formatted output
Why it exists
You want to print “Alex scored 87.5” where the name and the number come from variables. Concatenation makes a mess of it:
name = "Alex"
score = 87.5
print(name + " scored " + str(score)) # works, but it is noisy
f-strings — what this course uses
Put an f before the quote and write the variables straight into {}:
name = "Alex"
score = 87.5
print(f"{name} scored {score}")
Any expression can go inside the braces:
a, b = 7, 3
print(f"{a} + {b} = {a + b}")
print(f"{a} / {b} = {a / b}")
print(f"mean: {(87 + 92 + 65) / 3}")
Controlling the format
{value:spec} gives you decimals, width, alignment and base:
pi = 3.14159265
print(f"{pi:.2f}") # two decimal places
print(f"{pi:.4f}")
print(f"{pi:10.2f}") # total width 10, right aligned
print(f"{pi:<10.2f}|") # left aligned
print(f"{pi:^10.2f}|") # centered
n = 255
print(f"{n:b}") # binary
print(f"{n:o}") # octal
print(f"{n:x}") # lowercase hex
print(f"{n:X}") # uppercase hex
m = 5
print(f"{m:b}") # 101
print(f"{m:08b}") # zero-padded to 8 digits — handy for lining things up
# a small aligned table
data = [("Alex", 87.5), ("Blake", 92.0), ("Chen", 65.25)]
for name, score in data:
print(f"{name:<8}{score:>8.1f}")
One form that is excellent while debugging — {expr=} prints the expression and its value:
x = 42
y = [1, 2, 3]
print(f"{x=}")
print(f"{y=}")
Two older styles you will meet in other people’s code
Python has had three generations of formatting syntax. This course uses f-strings throughout, but you will run into the earlier two:
name, score = "Alex", 87.5
# first generation: C-style %
print("%s scored %.1f" % (name, score))
# second generation: str.format()
print("{} scored {:.1f}".format(name, score))
# third generation: f-string — use this
print(f"{name} scored {score:.1f}")
Same output all three times. What f-strings buy you is that each variable sits where it appears, so you never count placeholders against a list of arguments at the end.
print’s sep and end
print("a", "b", "c") # a space between them by default
print("a", "b", "c", sep="") # nothing between them
print("a", "b", "c", sep=" | ") # your own separator
print("2026", "09", "14", sep="-")
print("no newline", end="")
print(" ← continued")
Functions
Why they exist
Imagine a program a few thousand lines long, written straight down one file.
- Changing anything means guessing what else it might affect
- The same logic gets copy-pasted in ten places, and edits miss some of them
- Nobody else — including you in three months — can follow it
A function packages a piece of logic under a name, so you can invoke it by that name later. It is the most basic tool programming has for fighting complexity: cut a big problem into small ones and think about only one at a time.
You have been calling other people’s functions all along: print(), len(), int(), input(). Now write your own.
Three parts: name, parameters, return value
def area_of_circle(r): # the def keyword, a name, a parameter
return 3.14159 * r * r # the return value
print(area_of_circle(1))
print(area_of_circle(2.5))
defintroduces a definitionarea_of_circleis the name; the same rules apply as for variablesris a parameter — the function’s input- what follows
returnis the return value — the function’s output - the body is indented
Indentation decides what belongs to the function
Python marks blocks by indentation. Consecutive lines at the same indentation form one block.
def greet(name):
print("about to say hello") # indented — inside the function
print(f"hello, {name}") # indented — inside the function
print("not indented, so outside") # outside
greet("Alex")
The convention is four spaces per level. Never mix tabs and spaces — they look identical and Python will reject the file. VS Code converts tabs to spaces by default, so this rarely bites.
Defining is not running
def say_hi():
print("hi")
print("program starts")
# the function is defined above but has not run once
say_hi() # now it runs
say_hi() # and it can run as often as you like
A definition only registers the code. It does not execute it. Only a call — the name followed by parentheses — runs the body.
And the definition has to come first:
say_bye()
def say_bye():
print("bye")
A function is an object too
Every name in Python is a reference, and a function’s name is no exception. When Python reaches a def, it builds a function object from the body and points the name at it. So a function can be assigned to another name like any other value:
def square(n):
return n * n
f = square # no parentheses: not a call
print(f(7))
print(f is square)
print(type(square))
f = square is the same kind of thing as y = x: f and square now point at one function object.
Note there are no parentheses after square. Without them you get the function itself; with them you call it and get its return value. f = square(7) would call square and bind the result, 49, to f.
This is also why naming a variable list breaks things: list points at a built-in function, and list = [1, 2, 3] repoints it at a list, leaving the function out of reach.
Anything you can assign to a name, you can also pass to another function. In L6, sorted(words, key=len) does exactly that with len.
Passing arguments
The values in the parentheses at the call site are arguments, bound in order to the parameters from the definition:
def rectangle_area(width, height):
return width * height
print(rectangle_area(3, 4)) # width=3, height=4
print(rectangle_area(4, 3)) # different order — same answer here, but not in general
You can also name them, in which case order stops mattering:
def introduce(name, age, city):
print(f"{name}, {age}, from {city}")
introduce("Alex", 18, "Shanghai")
introduce(age=18, city="Shanghai", name="Alex") # named arguments
Inside and outside are separate
Variables created inside a function are local. They vanish when the function ends, and the outside world never sees them:
def compute():
result = 42 # local
return result
print(compute())
print(result) # error: there is no result out here
That separation is a feature. It means you can write a function without worrying about clashing with names used elsewhere:
def f():
x = "the x inside"
return x
x = "the x outside"
print(f())
print(x) # the function did not touch it
The full scoping rules (LEGB) are L14’s subject. “Inside and outside do not interfere” is enough for now.
return
return does two things: hands a value back to the caller, and ends the function immediately.
def check(n):
if n > 0:
return "positive"
return "not positive" # only reached if the return above did not fire
print(check(5))
print(check(-3))
Nothing after a return executes:
def demo():
print("this runs")
return 1
print("this never runs")
print(demo())
You can return several values at once — they come back packed in a tuple:
def min_max(a, b, c):
return min(a, b, c), max(a, b, c)
low, high = min_max(3, 9, 5)
print(low, high)
A function with no return gives back None:
def no_return():
print("I only print; I return nothing")
result = no_return()
print("returned:", result)
print("type: ", type(result))
print() versus return
This is the most common mistake beginners make, and the hardest for them to spot on their own. Read this section properly.
Both appear to “produce the answer,” but they do entirely different jobs:
print()is for a human — it puts characters on the screen. Once shown, it is gone; the program cannot get at itreturnis for the program — it hands a value back to the caller, which can go on computing with it
Two near-identical functions:
def add_print(a, b):
print(a + b) # only displays
def add_return(a, b):
return a + b # hands the value back
add_print(2, 3) # 5 appears on screen
add_return(2, 3) # nothing appears at all
The second one computed the right answer, and you see nothing — because it handed the value back and nobody caught it.
The difference is whether you can carry on:
def add_print(a, b):
print(a + b)
def add_return(a, b):
return a + b
x = add_print(2, 3) # 5 is displayed, but x is None
y = add_return(2, 3) # nothing is displayed, but y is 5
print("x =", x)
print("y =", y)
def add_print(a, b):
print(a + b)
# try to use the results and it falls apart completely
total = add_print(2, 3) + add_print(4, 5)
None + None is a TypeError.
There is a concrete consequence too: the lab platform marks your work by calling your function and comparing the value it returns. A function that prints instead of returning hands back None and scores zero — even when the algorithm is perfect and the screen output looks exactly right.
Using functions other people wrote
Python ships with a large collection of ready-made functions, grouped into modules. Bring one in with import:
import math
print(math.sqrt(16)) # square root
print(math.pi)
print(math.floor(3.7)) # round down
print(math.ceil(3.2)) # round up
print(math.factorial(5))
With math.pi available, that circle function gets more accurate:
import math
def area_of_circle(r):
return math.pi * r ** 2
print(f"{area_of_circle(1):.6f}")
How modules really work — where Python looks for them, how to install more, how to write your own — is L10. For now, import name followed by name.function() is all you need.
Summary
=binds; it is not mathematical equality. A Python variable is a reference: it holds the address of an object- A C variable is a piece of memory with a declared type; in Python the type lives on the object, so there is nothing to declare
- Integers and strings are immutable: “changing” one makes a new object, so sharing them is safe
id()gives the address,isasks whether two names are the same object. Use==for values;isonly againstNonea, b = b, aevaluates the right side in full first, so swapping needs no temporary- Give variables meaningful snake_case names, and never shadow
list,str,sumand friends - Strings compare lexicographically, uppercase before lowercase, and they are immutable
int()truncates;round()rounds, using banker’s rounding on exact halvesinput()always returns a string — convert it yourselfeval()is dangerous; useint()orfloat()when you want a number- Use f-strings for all formatting.
{v:.2f}for decimals,{v:>10}for alignment - A function is a name, parameters and a return value. Defining is not running, and definitions come first
- Functions are objects:
f = squaregives one function two names; parentheses are what call it - Locals do not escape the function
printis for people,returnis for the program. Inside a function,return
Exercises
- Write
celsius_to_fahrenheit(c)usingF = C * 9/5 + 32, returning the result to one decimal place. Note that it mustreturn, notprint. - Read two integers and print their sum, difference, product and quotient (two decimal places). Use f-strings, formatted as
12 + 5 = 17. - What is wrong with this? Fix it:
def double(n): print(n * 2) result = double(21) print(result + 1) - Write
initials(full_name)so that"Zhang San Feng"gives"Z.S.F.". (Hint:.split()breaks a string apart on spaces.) - Use f-strings to print the first three rows of a multiplication table, each product right-aligned in a field four characters wide.
- Explain why these two produce different results:
x = 10 def f(): x = 20 f() print(x) # what does this print? x = 10 def g(): return 20 x = g() print(x) # and this?
The lab problems for this lecture are in Lab 3.