Week 1
Week 1
Programmers are essentially problem solvers. They take a real-world problem and design
a step-by-step, logical solution that a computer can execute. The medium for this solution is
code, written in a programming language.
When solving problems, programmers can take different approaches, often falling into two
broad categories:
For a set of instructions to be considered an algorithm, it must have five key properties:
1. Finiteness: It must eventually terminate after a finite number of steps.
2. Definiteness: Each step must be precisely and unambiguously defined. There
should be no room for interpretation.
3. Input: It takes zero or more well-defined inputs.
4. Output: It produces one or more well-defined outputs.
5. Effectiveness: Each instruction must be basic enough that it can, in principle, be
carried out by a person using only a pencil and paper.
● Example of an Algorithm: A recipe for making a cup of tea.
○ Input: Tea bag, water, kettle, cup.
○ Steps:
■ Fill the kettle with water.
■ Boil the water in the kettle.
■ Place the tea bag in the cup.
■ Pour the boiled water from the kettle into the cup.
■ Wait for 2 minutes.
■ Remove the tea bag.
○ Output: A cup of tea.
This is an algorithm because it's finite (it ends), definite (each step is clear),
and effective.
● Example of what ISN'T an Algorithm: "Live a good life."
○ This is not an algorithm because it fails the definiteness property. The steps
are not precisely defined. What does "be kind" mean in every possible
situation? What is the exact procedure for "finding happiness"? It is
ambiguous and cannot be translated into a concrete, executable process.
3. Algorithms for Prime numbers
Let's develop an algorithm by directly applying the definition of a prime number.
Definition: A prime number is a natural number greater than 1 that has no positive divisors
other than 1 and itself.
Problem: Write an algorithm that takes an integer n as input and outputs True if n is prime,
and False otherwise.
To check if n is prime, we can check every number from 2 up to n-1 and see if any of them
divide n evenly. If we find even one such divisor, we know n is not prime. If we check all of
them and find no divisors, then n must be prime.
Python
def is_prime_v1(n):
"""Checks if a number is prime by testing divisibility from 2 to n-1."""
if n <= 1:
return False # Primes must be greater than 1
This algorithm works perfectly and is a direct translation of the definition. It demonstrates
how to turn a mathematical definition into a computational process. We can make it more
efficient, but its correctness comes from its faithful application of the core concept.
4. Key Progg Trends
The world of programming is constantly evolving. Two major trends are the
"democratization" of the field and a shift in the most valuable skills.
This refers to the process of making programming more accessible to everyone, not just
those with a formal Computer Science degree. Several factors drive this:
● High-Level Languages: Languages like Python use syntax that is closer to human
language, abstracting away complex details like memory management. This lowers
the barrier to entry.
● Open-Source Libraries: Programmers no longer need to build everything from
scratch. Need to create a chart? There's a library for that (Matplotlib). Need to do
complex math? There's a library for that (NumPy). You can stand on the shoulders of
giants.
● GenAI and Low-Code Tools: AI assistants like Gemini and tools like Zapier allow
people to create functional applications and automate tasks by describing what they
want in plain English, further lowering the technical bar.
As tools get better, the value is shifting from just being able to write code to higher-level
skills:
1. Problem Decomposition: The ability to take a large, vague problem and break it
down into smaller, manageable, and solvable pieces. This is the most crucial skill.
2. Systems Thinking: Understanding how your piece of code fits into the larger
system. It's not just about making your function work, but ensuring it works correctly
with the database, the user interface, and other services.
3. Critical Evaluation & Debugging: Especially with AI-generated code, you need the
skill to read, understand, and find flaws in code. You must be the expert who verifies
and fixes the AI's output.
4. Tool Proficiency: Mastering your tools—your code editor, version control (like Git),
and AI assistants—is essential for efficiency. The modern programmer orchestrates
tools rather than just typing characters.
5. Learning and Adaptation: The most important skill of all. The hot new technology of
today will be legacy tomorrow. You must be committed to continuous learning to stay
relevant.
5. How effective is GenAI at coding?
Generative AI is surprisingly effective at solving common coding problems, especially
those found in competitive programming contests. These contests provide an excellent
benchmark because the problems are well-defined, have clear correct answers, and are
ranked by difficulty.
In short, GenAI is like a very knowledgeable student who has memorized every textbook but
sometimes lacks the deep understanding to solve a problem they've never seen before.
6. Refuting an algorithm
To refute an algorithm, you don't need to prove it's wrong for all inputs. You only need to find
one single counterexample.
A counterexample is a specific, valid input for which the algorithm produces an incorrect
output, fails to stop, or crashes.
1. Think about the algorithm's weakness: The algorithm completely ignores all the
numbers in the middle of the list.
2. Construct a counterexample: Create a list where the largest number is not at the
beginning or the end.
○ Let's try the list: [10, 50, 20]
3. Test the algorithm with the counterexample:
○ Input: [10, 50, 20]
○ Algorithm's Steps:
■ Get the first number: 10.
■ Get the last number: 20.
■ Compare them: 20 is greater than 10.
○ Algorithm's Output: 20.
4. Compare with the correct output:
○ The actual maximum value in [10, 50, 20] is 50.
5. Conclusion: The algorithm's output (20) does not match the correct output (50).
Therefore, the list [10, 50, 20] is a valid counterexample that refutes the proposed
algorithm.
● The Problem of Licensing: Open-source code is free to use, but not without rules.
Licenses like the GPL (GNU General Public License) have a "copyleft" provision: if
you use GPL-licensed code in your project, your project must also be open-sourced
under the GPL. In contrast, permissive licenses like MIT or Apache have very few
restrictions.
● The Misuse: AI models are trained on all of this code, regardless of license. When
the AI then generates code for a user, it might produce a snippet that is a derivative
of GPL-licensed code it saw during training. If a company uses this AI-generated
snippet in their proprietary, closed-source product, they could be in violation of the
original GPL license without even knowing it.
● The Debate: This has led to lawsuits and intense debate. Is an AI learning from code
the same as a human learning from it? Or is it a form of automated copyright and
license laundering? The legal and ethical frameworks are still struggling to catch up
with the technology.
Let's say you ask an AI: "Write a Python function to calculate the average of a list of
numbers."
Python
# AI-generated code
def calculate_average(numbers):
return sum(numbers) / len(numbers)
This code is clean, simple, and correct... for the "happy path." But it makes several
dangerous assumptions:
● It assumes the list numbers will never be empty. If you pass it an empty list [],
len(numbers) will be 0, and the code will crash with a ZeroDivisionError.
● It assumes the list will only contain numbers. What if someone passes [1, 2,
'apple']? The sum() function will fail.
The AI doesn't automatically consider these "edge cases" because it doesn't possess true
understanding. It simply reproduces the most common pattern associated with the request.
A professional developer's job is to think beyond the happy path. Before writing (or
accepting) this code, they would ask clarifying questions to understand the full requirements.
This is a critical skill when using AI. You must act as the human supervisor.
Instead of just accepting the AI's code, you should "interview" the stakeholder (or even
prompt the AI with these constraints). Good questions for the average-calculator problem
would be:
● "What is the expected output if the input list is empty? Should it be 0, an error, or
something else?"
● "What should the function do if the list contains non-numeric data? Should it ignore
them, or raise an error?"
● "Are the numbers always integers, or can they be floating-point numbers? Is there a
required precision for the result?"
By asking these questions, you move from a fragile, assumption-based solution to a robust,
well-defined one. This critical thinking is what currently separates a human developer from
an AI code generator.
Bash
$ python3
Python 3.11.4 (main, Jun 7 2023, 10:13:09) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> # This is the REPL prompt. Let's try some code.
>>>
>>> 2 + 2
4
>>>
>>> message = "Hello, playground!"
>>> print(message)
Hello, playground!
>>>
>>> len(message)
17
>>>
>>> # You can test how functions work
>>> message.upper()
'HELLO, PLAYGROUND!'
>>>
>>> # To exit the REPL, you can type exit() or press Ctrl-D
>>> exit()
$
12. Basic Arithmetic Operators
Python provides a standard set of arithmetic operators to perform calculations, just like a
regular calculator. Here's a look at the most common ones through simple expressions.
● + (Addition): Adds two numbers.
○ 10 + 5 # Result: 15
● - (Subtraction): Subtracts the right number from the left.
○ 10 - 5 # Result: 5
● * (Multiplication): Multiplies two numbers.
○ 10 * 5 # Result: 50
● / (True Division): Divides the left number by the right. The result is always a
floating-point number (a number with a decimal).
○ 10 / 3 # Result: 3.3333333333333335
● // (Floor Division): Divides and then rounds the result down to the nearest whole
number (integer).
○ 10 // 3 # Result: 3
○ 11 // 3 # Result: 3
○ -10 // 3# Result: -4 (rounds down, away from zero)
● % (Modulo): Returns the remainder of a division. This is incredibly useful for
checking if a number is even or odd.
○ 10 % 3 # 10 divided by 3 is 3 with a remainder of 1. Result: 1
○ 12 % 2 # 12 is even, so the remainder is 0. Result: 0
○ 13 % 2 # 13 is odd, so the remainder is 1. Result: 1
● ** (Exponentiation): Raises the left number to the power of the right.
○ 2 ** 3 # Same as 2 * 2 * 2. Result: 8
○ 5 ** 2 # Same as 5 * 5. Result: 25
The official Python documentation has a complete table of operator precedence, which is the
ultimate source of truth for any complex case.
Comprehending a Complex Expression: Step-by-Step
Final Result: 17
Rule of Thumb: When in doubt, use parentheses (). They make the order of operations
explicit and your code easier for others (and your future self) to read. The expression (5 + (2
* (3**2))) - (7-1) is much clearer, even though it yields the same result.
In many programming languages (like C or Java), an integer type has a fixed number of bits
(e.g., 32 or 64). This means there's a maximum number they can store. If you try to add 1 to
the maximum possible integer, it can "overflow" and wrap around to a very small negative
number, causing a serious bug.
● Python's Advantage: Python is special in this regard. Its standard integers (int) have
arbitrary precision. This means they can grow to use as much memory as needed
to store a number, so you won't experience overflow errors with standard integers in
Python.
2. Floating-Point Inaccuracy (The Real Problem)
This is a much more common and subtle limitation. Floating-point numbers (float) are used
to represent numbers with decimal points (e.g., 3.14159, 0.1). They are stored in a binary
format (IEEE 754 standard) that can't precisely represent all decimal fractions, just like we
can't write 1/3 perfectly as a finite decimal.
● The Classic Example: The number 0.1 is a simple, finite decimal for us, but it's an
infinitely repeating fraction in binary. The computer has to cut it off at some point,
leading to a tiny representation error.
● Demonstration in the REPL:
● Python
Why this matters: For most applications, this tiny error is irrelevant. But in scientific,
financial, or engineering calculations where high precision is critical, these small errors can
accumulate over millions of calculations and lead to significant and incorrect results. For
these cases, programmers use specialized tools like Python's Decimal module.
Python
>>> import sys # First, we must import the 'sys' module to use it
>>>
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308,
min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-308, dig=15, mant_dig=53,
epsilon=2.220446049250313e-16, radix=2, rounds=1)
Understanding Key Attributes from the Documentation
By looking at this output (and consulting the Python documentation), we can understand
some of the key limits of floating-point numbers on our system:
● max: 1.797...e+308
○ This is the maximum representable float. Any number larger than this will
be considered "infinity."
○ 1.79e+308 is scientific notation for 1.79 followed by 308 zeros. It's a massive
number.
● min: 2.225...e-308
○ This is the smallest positive normalized float. Any positive number smaller
than this is effectively treated as zero.
● epsilon: 2.220...e-16
○ This is arguably the most important attribute for understanding precision.
Epsilon is the smallest possible difference between 1.0 and the next
representable floating-point number.
○ In other words, 1.0 + epsilon will result in a number slightly different from 1.0,
but 1.0 + (epsilon / 2) will still evaluate to 1.0 because the change is too small
to be registered. This number quantifies the precision limit of float arithmetic.
○ Let's test this!
● Python
This investigation confirms that our computer's arithmetic has finite precision and provides
us with the exact values that define its boundaries.
16. Binary digits (bits)
The most fundamental concept in all of computing is that all data—numbers, text, images,
videos—is stored as sequences of binary digits, or bits.
● A bit is the smallest unit of data and can only have one of two values: 0 or 1.
Think of a bit as a single light switch. It can either be Off (0) or On (1). There are only two
possible states. This two-state system is easy to represent physically in computer hardware:
With just one bit, you can only represent two things. To represent more complex information,
we string bits together. Each time we add a bit, we double the number of distinct
combinations we can make.
● 1 bit: 2 combinations
○ 0
○ 1
● 2 bits: 4 combinations (2 * 2 = 2^2)
○ 00
○ 01
○ 10
○ 11
● 3 bits: 8 combinations (2 * 2 * 2 = 2^3)
○ 000
○ 001
○ 010
○ 011
○ 100
○ 101
○ 110
○ 111
● 8 bits (a Byte): 256 combinations (2^8)
This is the foundation of all digital representation. The number 7 might be stored as
00000111, the letter 'A' might be stored as 01000001, and a single pixel in a black-and-white
image might be stored as 0 (white) or 1 (black). Everything boils down to these simple 0s
and 1s.
17. Naming Things
This section connects the concept of bits to a practical problem: how many bits are needed
to uniquely identify a certain number of distinct items?
If you have N distinct items, what is the minimum number of bits required to give each item a
unique label?
The Rule
The number of bits needed is the smallest integer b such that 2^b >= N.
With 5 bits, you could assign 'a' to 00000, 'b' to 00001, 'c' to 00010, and so on, up to 'z' which
would be 11001.
Certain powers of 2 appear so frequently in computing that they are worth remembering.
They often define the limits or sizes of common data types.
Friend's Statement 1: "Computers use binary because it's just simpler for them."
● Critique: This statement is true on the surface but misses the critical "why." It's not
about simplicity in a cognitive sense (like an easy math problem). The real reason is
physical reliability.
● Deeper Explanation: Electronic circuits are built from transistors, which act like tiny
switches. It is vastly more reliable and less prone to error to design a circuit that only
has to distinguish between two physical states (e.g., a low voltage for '0' and a high
voltage for '1') than it would be to design a circuit that has to reliably distinguish
between ten different voltage levels for a base-10 system. A slight fluctuation in
power could easily be misread in a 10-state system, but it's much less likely to cause
a high voltage to be mistaken for a low one. The choice of binary is a direct
consequence of engineering for robustness and fault tolerance in physical hardware.
Friend's Statement 2: "My computer has a 64-bit processor, so it can only handle numbers
up to 2^64 - 1."
● Critique: This is a very common and understandable confusion between the
computer's architecture and how a high-level language like Python works. The
statement is false for Python integers.
● Deeper Explanation:
○ What "64-bit" really means: A 64-bit architecture means the processor's
internal data pathways (registers) and memory addresses are 64 bits wide.
This makes it very efficient at performing calculations on numbers that fit
within 64 bits and allows it to access a massive amount of RAM.
○ Python's Abstraction: Python's int type is not a raw 64-bit integer. It is a
more complex object that can use as much memory as needed to represent a
number of any size. If you calculate a number larger than 2^64 - 1, Python will
automatically allocate more memory to store it. This is called
arbitrary-precision arithmetic. It might be slightly slower than a native 64-bit
operation, but it prevents overflow errors and makes the language much
easier to use.
○ In other languages (like C): In a language like C, this statement would be
true. If you define a standard 64-bit integer (uint64_t) and add 1 to its
maximum value, it will overflow. This is a key difference that highlights the
abstractions high-level languages provide.
19. Limits to float computation
We've seen that floating-point computation has limitations, but it's crucial to understand that
the primary issue is limited precision, not just a limited range.
Think of the number line. In pure mathematics, there are an infinite number of values
between any two numbers, say 1.0 and 1.1. But a computer using floating-point numbers
cannot store an infinite number of values. It has a finite number of bits (usually 64) to
represent a number.
This means there are "gaps" between representable floating-point numbers. The size of
these gaps changes:
● The gaps are very small for numbers close to zero.
● The gaps get larger as the numbers get larger.
When the result of a calculation falls into one of these gaps, it must be rounded to the
nearest representable number. This introduces a tiny error.
This might seem insignificant, but these small errors can accumulate in calculations that
involve many steps.
Imagine a simulation where you add a very small value (0.0000001) to a running total a
billion times.
Python
total = 0.0
increment = 0.0000001
iterations = 1_000_000_000
for _ in range(iterations):
total += increment
Each addition introduced a minuscule rounding error. After a billion additions, those tiny
errors compounded into a noticeable discrepancy. This is a critical issue in fields like
scientific computing, financial modeling, and game physics, where small, repeated errors
can lead to completely wrong outcomes.
An object is a self-contained entity that consists of both data (attributes) and procedures
to manipulate that data (methods). It's a way of abstracting and modeling a real-world
entity inside a program.
An object in Python bundles these two things together. The accelerate() method inherently
knows how to modify the current_speed attribute of that specific car object.
Objects in Practice
You use objects constantly in Python, even with the simplest data types.
Let's see this in the REPL using the built-in type() function.
Python
>>> type(5)
<class 'int'>
This tells us that the number 5 is not just a value; it's an instance of the int class (an integer
object).
Python
>>> type("hello")
<class 'str'>
Because "hello" is an object, it comes with built-in methods to manipulate its data:
Python
This object-oriented nature makes Python code more organized and intuitive, as it allows us
to model complex systems by thinking about the "things" (objects) involved and what they
can do.
In Python, all textual data is represented by the str object. This object handles all the
complexities of character encoding for you, so you can think about text as "a sequence of
characters" rather than "a sequence of numbers."
😊
just 1 byte (8 bits), making it backward compatible.
■ For other characters (like '€' or ' '), it uses 2, 3, or even 4 bytes.
Python 3's str object is Unicode by default. This is a major advantage, as it allows your
programs to handle text from any language seamlessly without you needing to worry about
the underlying encoding in most cases.
22. Strings
A Python str object is an immutable sequence of Unicode characters. The key idea is that
while we see letters, Python sees the underlying integer code points.
Python provides two built-in functions, ord() and chr(), that allow us to easily move between
the character representation and its integer code point, making this abstraction visible.
● ord(): Takes a single character (a string of length 1) and returns its integer Unicode
code point.
● chr(): Takes an integer and returns the corresponding Unicode character as a string.
Python
>>> # Get the integer code point for the letter 'A'
>>> ord('A')
65
>>>
>>> # Get the integer code point for the letter 'B'
>>> ord('B')
66
>>>
>>> # Get the integer code point for a lowercase letter 'a'
>>> ord('a')
97
This shows that uppercase and lowercase letters have different numerical values. The letters
of the alphabet are represented by consecutive integers.
Python
This ability to translate characters to and from integers is the foundation of all text
processing on a computer. Every string operation, from sorting text alphabetically to
changing a letter's case, is ultimately performed by manipulating these underlying numbers.
Python
This demonstrates how Unicode, via the str object, handles a much wider range of
characters than simple ASCII.
23. Summary
This section summarizes the key concepts from topics 11 through 22, covering the
fundamentals of working with data in Python.
● The REPL is your playground: The Read-Evaluate-Print Loop is an interactive tool
for instantly testing code snippets, exploring how functions work, and performing
quick calculations.
● Numbers and Arithmetic: Python provides standard arithmetic operators (+, -, *, /,
//, %, **) that follow the PEMDAS order of operations. Parentheses () should be used
to ensure clarity.
● Computation has limits: All computer arithmetic is limited by the finite memory used
to store numbers. The most important limitation in Python is the finite precision of
floating-point numbers (float), which cannot represent all decimal values perfectly.
This leads to small rounding errors that can accumulate in large calculations. The
sys.float_info object allows you to inspect these limits.
● Everything is Binary: All data on a computer is stored as sequences of 0s and 1s
(bits). The number of distinct items you can represent is determined by the number of
bits you use (2^b items for b bits).
● Python's Abstractions: Python abstracts away many of the computer's underlying
complexities.
○ Its int objects have arbitrary precision, meaning they can grow as large as
needed, unlike integers in many other languages.
○ Everything in Python is an object, which bundles data (attributes) and
behaviors (methods) together.
● Text is Numbers: Textual data is stored by mapping each character to a unique
integer code point. Python's str object uses the Unicode standard, allowing it to
represent characters from virtually all human languages. The ord() and chr()
functions let you convert between a character and its integer code point, revealing
this underlying representation.