MIT 6.0001 learning journal
Introduction to Computer Science and Programming in Python
This post is part of my lecture-by-lecture review. Each entry captures the mental models, Python mechanics, mistakes to watch for, and a short practice set I can return to later.
Lecture 4 completed - one third of the 12-lecture series.
Lecture 4 is the point where programming starts to feel less like writing instructions from top to bottom and more like designing a system. The syntax of a function is important, but the larger lesson is how functions help control complexity.
Dr. Ana Bell connects functions to two design ideas: decomposition, which divides a problem into smaller parts, and abstraction, which lets us use a part through a clear interface without keeping every implementation detail in mind.
1. Two tools for managing complexity
01
Decomposition
Break one large problem into smaller, self-contained modules. Each module performs a focused job and can be developed, tested, and reused independently.
Design question: What smaller responsibilities make up this problem?
02
Abstraction
Hide internal details behind a dependable interface. A caller needs to understand what a function expects and promises, not every line used to produce the result.
Design question: What does a caller need to know to use this correctly?
The two ideas work together. Decomposition decides where to create boundaries; abstraction makes those boundaries useful. A well-designed function becomes a small black box: provide valid inputs, rely on its specification, and use the returned result.
This is also why a shorter program is not automatically a better program. Good structure is measured by clarity and useful functionality, not by cramming the most work into the fewest lines.
2. The anatomy of a Python function
def is_even(number):
"""Return True when number is divisible by 2."""
return number % 2 == 0
answer = is_even(8)
print(answer) # True
defTells Python that a function definition is beginning.is_evenNames the operation. A descriptive verb or question makes calling code easier to read.numberIs the formal parameter: a local name that will receive a value when the function is called.returnEnds the call and sends a value back to the expression that called the function.
In is_even(8), the value 8 is the actual argument. Python maps that value to the formal parameter number inside the new function call. The call then evaluates to True, so answer receives that Boolean value.
3. A specification creates the abstraction
A function name gives the first clue, but the specification is what allows another programmer to use it confidently. A useful docstring answers three questions:
- Inputs: What values and types may the caller provide?
- Behaviour: What does the function compute or change?
- Output: What value and type will the function return?
def rectangle_area(width, height):
"""Return the area of a rectangle.
width and height are non-negative numbers.
"""
return width * height
A caller can use rectangle_area(5, 3) without knowing whether the body uses multiplication directly, delegates to another helper, or is later optimized. If the contract stays the same, the implementation can change without forcing the caller to change.
4. return is not the same as print()
return
Sends a value back to the caller. The value can be assigned, tested, combined with another expression, or passed to another function.
print()
Displays text in the console. It is useful for communication and debugging, but displaying a value does not give that value back to the caller.
def double_with_print(number):
print(number * 2)
def double_with_return(number):
return number * 2
printed_result = double_with_print(6) # displays 12
returned_result = double_with_return(6) # stores 12
print(printed_result) # None
print(returned_result) # 12
If execution reaches the end of a function without an explicit return, Python returns None. None is a special value of type NoneType representing the absence of a value; it is not the string "None".
5. Function calls create their own scope
Scope describes the environment in which a name is bound to a value. The main program has a global scope. Every function call creates a new local scope with its own parameter bindings and local variables.
x = 10
def add_one(x):
x = x + 1
return x
result = add_one(x)
print(x) # 10
print(result) # 11
The two variables named x live in different environments. The global x supplies the value 10 to the call. Inside add_one, the local x changes to 11. When the function returns, its local environment is discarded, but the returned value is saved in result.
Python can read a name from an enclosing or global scope when that name is not defined locally. Reassigning an outer variable from inside a function is different and can make code difficult to reason about. The lecture recommends avoiding global-variable shortcuts when clean parameters and return values can express the same data flow.
6. Functions are objects too
Python values such as integers, strings, and functions are objects. That means a function can be passed as an argument to another function and called later.
def apply_twice(function, value):
return function(function(value))
def add_one(number):
return number + 1
result = apply_twice(add_one, 5)
print(result) # 7
Notice the difference between add_one and add_one(5). The first refers to the function object. The second calls the function immediately. Here, apply_twice receives the function itself, then decides when and how to call it.
This idea becomes important later in programming: callbacks, sorting keys, decorators, event handlers, and many data-processing tools all rely on treating behaviour as a value.
7. Decomposition in practice
Suppose I need to turn raw quiz marks into a short report. One long block could clean the values, calculate an average, decide whether the student passed, and format the message. A decomposed version gives each responsibility a name:
def calculate_average(scores):
return sum(scores) / len(scores)
def passed_course(average, passing_mark):
return average >= passing_mark
def build_report(scores, passing_mark):
average = calculate_average(scores)
passed = passed_course(average, passing_mark)
return average, passed
average, passed = build_report([78, 84, 91], 50)
The top-level build_report function reads almost like an outline of the solution. Each helper can be tested with small inputs, and its implementation can change without rewriting the whole program. That is decomposition and abstraction working together.
Practice lab
MIT's in-class questions focus on tracing function calls, separating output from returned values, and passing functions as arguments. I turned those themes into the following original self-checks. Try each one before opening the explanation.
01 Predict the output: print versus return
def add(left, right):
return left + right
def show_product(left, right):
print(left * right)
print(add(2, 3))
print(show_product(4, 5))
Answer: Python displays 5, then 20, then None. show_product prints 20 inside the function but has no explicit return, so the outer print() displays its returned None.
02 Identify the formal parameters and actual arguments
def power(base, exponent):
return base ** exponent
answer = power(3, 4)
Answer: base and exponent are formal parameters in the definition. 3 and 4 are actual arguments in the call. Python maps base to 3 and exponent to 4.
03 Trace the scopes
count = 4
def bump(count):
count = count + 2
return count
new_count = bump(count)
Answer: The global count remains 4. The local parameter becomes 6, and that value is returned into new_count. After the call, new_count is 6.
04 Pass a function as an argument
def transform(function, value):
return function(value + 1)
def square(number):
return number ** 2
print(transform(square, 3))
Answer: The result is 16. transform first calculates 3 + 1, then calls square(4).
05 Write a function from a specification
Write is_in_range(value, lower, upper). It should return True when value is between the two limits, including both endpoints.
def is_in_range(value, lower, upper):
"""Return True when lower <= value <= upper."""
return lower <= value and value <= upper
The key is to return the Boolean expression instead of printing it.
Key takeaways
- Decomposition breaks a program into focused modules; abstraction hides a module's details behind a clear contract.
- A Python function combines a name, parameters, a specification, a body, and a returned result.
- Formal parameters are names in the definition; actual arguments are values supplied in the call.
- Every function call creates a local scope, and its local variables do not automatically overwrite variables in the global scope.
returnproduces a reusable value, whileprint()only displays output.- A function without an explicit return produces
None. - Functions are Python objects, so they can be passed into and returned from other functions.
- Small, well-specified functions make programs easier to test, debug, reuse, and review.