Un-tit-led

Source

University of Helsinki: Python Programming MOOC 2026
Introduction to Programming course (BSCS1001, 5 ECTS) is now Complete. Review here.

This has to be one of the best courses out there and I can't believe it's free while also having an exam with graded marks and certificate.

I was so confused about dictionaries, single iterables, tuples before now I just took an intro exam and I basically ace-d the whole exam.

Hands-on is definately the way to go.

Learnings

"Read the questions completely first. You always over-engineer when the questions are very simple.""

"DON'T TRY TO CRACK THE GAME OR THINK YOU ARE GOING TOO FAR IF YOU ARE THINKING IN A DIFFERENT WAY THAT INTENDED. SOLVE IT HOW YOU BEST THINK IS BEST.""

READ THE DAMN QUESTION! YOU ALMOST ALWAYS ASSUME A HARDER QUESTION THAT IS BEING ASKED, SOMETIMES, THE OPPOSITE

len() is better than looping and counting in dictionary

len() reads the dictionary's size directly from the object, in constant time — it doesn't iterate at all internally.
-LLM(Please verify)

Idiomatic: peculiar to a particular group, individual, or style

"this is what a Python code reviewer would expect to see" than a hard rule.
-LLM(Please Verify)

Single line iterables are preferred as per PEP

As per PEP, single line iterables are preferred.


  # your style
  total = 0
  for item in data.values():
      total += item['students']
  
  # generator expression style
  total = sum(item['students'] for item in data.values())
The critical mechanical difference from your for-loop: the generator expression does not run all at once and build something. It runs item-by-item, only as sum() asks for the next value. sum() asks for one, adds it, asks for the next, adds it, etc. Nothing is stored — there's no intermediate total variable you manage yourself; sum() manages that internally.
-LLM(Please verify)
It is proposed to allow conditional construction of list literals using for and if clauses. They would nest in the same way for loops and if statements nest now.

PEP 202:: List comprehensions provide a more concise way to create lists in situations where map() and filter() and/or nested loops would currently be used.


>>> print [i for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> print [i for i in range(20) if i%2 == 0]
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

>>> nums = [1, 2, 3, 4]
>>> fruit = ["Apples", "Peaches", "Pears", "Bananas"]
>>> print [(i, f) for i in nums for f in fruit]
[(1, 'Apples'), (1, 'Peaches'), (1, 'Pears'), (1, 'Bananas'),
  (2, 'Apples'), (2, 'Peaches'), (2, 'Pears'), (2, 'Bananas'),
  (3, 'Apples'), (3, 'Peaches'), (3, 'Pears'), (3, 'Bananas'),
  (4, 'Apples'), (4, 'Peaches'), (4, 'Pears'), (4, 'Bananas')]
>>> print [(i, f) for i in nums for f in fruit if f[0] == "P"]
[(1, 'Peaches'), (1, 'Pears'),
  (2, 'Peaches'), (2, 'Pears'),
  (3, 'Peaches'), (3, 'Pears'),
  (4, 'Peaches'), (4, 'Pears')]
>>> print [(i, f) for i in nums for f in fruit if f[0] == "P" if i%2 == 1]
[(1, 'Peaches'), (1, 'Pears'), (3, 'Peaches'), (3, 'Pears')]
>>> print [i for i in zip(nums, fruit) if i[0]%2==0]
[(2, 'Peaches'), (4, 'Bananas')]
        

Generator expressions are especially useful with functions like sum(), min(), and max() that reduce an iterable input to a single value

PEP 289
max(len(line) for line in file if line.strip())
List comprehensions greatly reduced the need for filter() and map(). Likewise, generator expressions are expected to minimize the need for itertools.ifilter() and itertools.imap(). In contrast, the utility of other itertools will be enhanced by generator expressions:
dotproduct = sum(x*y for x,y in itertools.izip(x_vector, y_vector))

String↔List


str.split(sep=None, maxsplit=-1)

'1,2,3'.split(',') → ['1', '2', '3']
'1,2,3'.split(',', maxsplit=1) → ['1', '2,3']
'1,2,,3,'.split(',') → ['1', '2', '', '3', '']
'1<>2<>3<4'.split('<>') → ['1', '2', '3<4']

At most maxsplit is done is specified.

list to string with join

', '.join(['spam', 'spam', 'spam'])     → 'spam, spam, spam'
'-'.join('Python')                      → 'P-y-t-h-o-n'
    

Split Strings into lists

def exam_and_exercise_completed(inpt):
      space = inpt.find(" ")
      exam = int(inpt[:space])
      exercise = int(inpt[space+1:])
      return [exam, exercise]

Better Boundary: Grading System


def grade(points):
boundary = [0, 15, 18, 21, 24, 28]
for i in range(5, -1, -1):
    if points >= boundary[i]:
        return i
  

Initialize a list with predefined length and copying a row

 grades = [0] * 6
Note from LLM(Cross-check once): copy_sudoku = [[]] * 9 does not create 9 separate empty lists, it creates one empty list and stores 9 references to it.
The correct way is: copy_sudoku = [[] for _ in range(9)]

N.B: For merely copying a row, you can also use copy_sudoku = [row[:] for row in sudoku] which is equivalent to:
      copy_sudoku = []
      for row in sudoku:
        copy_sudoku.append(row[:])
    

Arithmetic Pitfalls and shortcuts

  • // is floor division. / is fractional and % is modulus(remainder). Should be enough for any weird math int-float, round up/down, non-sense
  • exponential is with ** so 2**3 = 8

pop removes by index. remove removes by value(first one only)

Removing items from a list:

      
my_list.pop(indexOfItemToRemove)
my_list.remove(valueOfItemToRemove → First Occurrence only)
      
    
List now shortens. Pop also returns the removed item.

string IS NOT A TYPE in python, str is. Also, it's True & False not true and false

Use built in functions like min, max & sum (sum takes in iterable)

sum takes iterables, not just numbers normally just like min and max but you can't just give numbers as arguments directly in sum unlike min and max.

LLM(Please cross-check): Better way to write code

  • Debugger not starting in the current directory but in workspace directory

    Just add: , "cwd": "${fileDirname}" line in the configuration like so:

    
                    {
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "cwd": "${fileDirname}"
        }
    ]
    }
                  

  • Use math.floor or // instead of int() for "round down"

    int() rounds toward zero, not down — for positive numbers they're the same, but // (floor division) is the idiomatic way to say "divide and round down".

  • Use a with block for the request

    It's more idiomatic to close it explicitly:

    
    with urllib.request.urlopen(url) as response:
        data = json.loads(response.read())
    
    #instead of
    my_request = urllib.request.urlopen(
      "https://studies.cs.helsinki.fi/stats-mock/api/courses"
    )
    data = json.loads(my_request.read())
                  

  • If unsure that a sum can contain none, use better code

    One small note: your sum(item["exercises"]) assumes exercises is a flat list of numbers with no None/falsy entries — the model solution guards against that with if exercise: inside its loop. Worth checking the actual data shape; if exercises can contain None values, you'd want sum(e for e in item["exercises"] if e) instead of a bare sum().

  • Dict literal instead of building up course_details key by key

    
    #Instead of
    course_details = {}
    
    course_details["weeks"] = len(data)
    course_details["students"] = max(item["students"] for item in data.values())
    course_details["hours"] = sum(item["hour_total"] for item in data.values())
    course_details["hours_average"] = (
        course_details["hours"] // course_details["students"]
    )
    course_details["exercises"] = sum(item["exercise_total"] for item in data.values())
    course_details["exercises_average"] = (
        course_details["exercises"] // course_details["students"]
    )
    return course_details
    
    #do
    students = max(week["students"] for week in data.values())
    hours = sum(week["hour_total"] for week in data.values())
    exercises = sum(week["exercise_total"] for week in data.values())
    
    return {
      "weeks": len(data),
      "students": students,
      "hours": hours,
      "hours_average": hours // students,
      "exercises": exercises,
      "exercises_average": exercises // students,
    }
                      

Part 1

This course has reminded me how important recall is over recognition and the reason why many "video only lectures/courses" fail at it.
You feel you are getting something, sometimes smugly, and never get to practice them. And when it comes to actually trying to solve a problem you come up blank.

To be very clear, mooc.fi's Java hands on Java course(still incomplete BTW) was the reason I qualified for a job interview: #TODO insert video here once moved

Part 2

  • Statement is a part of the program which executes something.
  • Block is a group of consecutive statements that are at the same level in the structure of the program.
  • Expression is a bit of code that reulsts in a determined data type.
  • Function executes some functionality, they also can take arguments/parameters.
  • else branch is not mandatory especially when a lot of elif are involved. However, else incompasses all the conditions not accounted for so it's still a good check for outside expected cases.
  • #TODO: ?: statement
  • x>=a & x<=b can be rewritten as a<=x <=b , although not higely used as it's missing from other langauges
  • The order of conditional statements especially involving loops is a common source of bugs. Debugging is often the simplest way to finding their cause.

PART 3

PART 4

PART 5

3. Dictionary 4. Tuple

PART 6

1. Reading files

2. Writing files
  • We can create a new file but also append data to an opened file with an extra argument.
  • Use w parameter while opening file to signify you will write to the file also.
      with open("new_file.txt", "w") as my_file:
        # code to write something to the file
        my_file.write("Hello there!")
    NB: if the file already exists, all the contents will be overwritten. It pays to be very careful when creating new files.
  • write function doesn't add extra stuff at the end unlike print.
  • a can be used while opening new file to add new lines to it.
    with open("new_file.txt", "a") as my_file:
      my_file.write("This is the 4th line\n")
      my_file.write("And yet another line.\n")
  • In practice however, appending data to file is not a very common task. More often the file is read, processed and overwritten in entirety.
    For example, when the contents should change in the middle of the file, it is usually easiest to overwrite the entire file.
  • Clearing file contents: Simply open in write mode and close immediately:
    
        with open("file_to_be_cleared.txt", "w") as my_file:
          pass
    
    Or, simply:
    
    open('file_to_be_cleared.txt', 'w').close()
      
  • Python doesn't allow empty blocks so pass is needed at places.
  • To delete a file, we need help from the operating system:
    
    # the command to delete files is in the os module
    import os
    
    os.remove("unnecessary_file.csv")
      
    Note: For the tests in TMC, clear files instead of deleting them.
  • Each functioned defined is relatively simple, and they all have a single responsibility. This is a common and advisable approach when programming larger wholes.
    The single responsibility principle makes verifying functionality easier. It also makes it easier to make changes to the program later, and to add new features.
    But also, it makes it writing programs not confusing and irritating.
  • In a well designed program, changes to a certain functionality will only select some sections of the code and it will be easier to determine where the changes should be made.
  • If the code for this single functionality was implemented in multiple places, there would be a definite risk that we would not remember to change all the instances when changing the functionality.
  • text.startswith(prefix,start,end) is roughly equivalent to text[start:end].startswith(prefix)
  • 3. Handling errors
  • Two types of errors:
    1. Syntax errors, which prevent the execution of program
      They are easy to fix as the Interpreter flags the error location for you.
    2. Runtime errors, which halt the execution
      Are a bitch and harder to spot and may happen in certain circumstances and especially in marginal cases.
  • Many errors during execution of program is simply due to invalid inputs like:
    • missing/empty input values in mandatory fields.
    • negative where positive value is expected
    • missing files or typos in filenames
    • values that are too small or too large, for example when working with dates and times.
    • invalid indexes, such as trying to access index that out of range.
    • value of a wrong type
  • Exceptions: Errors that occur while the program is already running are called exceptions.
    It is possible to prepare for exceptions, and handle them so that the execution continues despite them occurring.
  • Exception handling is accomplished by try and except statements.
  • If something in the try block causes an exception, Python checks if there is a corresponding except block. If such block exists, that is executed and the program continues as if nothing happened.
  • try-except example:
    
        def read_integer():
        while True:
            try:
                input_str = input("Please type in an integer: ")
                return int(input_str)
            except ValueError:
                print("This input is invalid")
    
    number = read_integer()
    print("Thank you!")
    print(number, "to the power of three is", number**3)
      
  • Sometimes it is enough to catch exceptions with a try-except structure, without doing anything about them. That is, we can just ignore the situation in the except block with a pass.
    Python does not allow empty blocks, so the command is necessary.
  • Typical Errors:
    1. ValueError: Arguments passed is somehow invalid like float("1,23") as , is not expected as a decimal separator.
    2. TypeError.
    3. IndexError
    4. ZeroDivisionError
    5. Exceptions in file handling: FileNotFoundError, io.UnsupportedOperation or PermissionError.
  • Multiple errors at once:
    
        try:
        with open("example.txt") as my_file:
            for line in my_file:
                print(line)
        except FileNotFoundError:
            print("The file example.txt was not found")
        except PermissionError:
            print("No permission to access the file example.txt")
      
  • Sometimes it is not necessary to specify the error the program prepares for. Especially when dealing with fils, it is often enough to know an error has occurred and safely exit the program:
    
    try:
        with open("example.txt") as my_file:
            for line in my_file:
                print(line)
    except:
        print("There was an error when reading the file.")
      
    N.B: NB: the except statement here covers all possible errors, even those caused by the programming mistakes. Only syntax errors will not be caught by this, as they prevent the code from being executed in the first place.

    For example, the following program will always throw an error, because the variable name my_file is written as myfile on the third line.
    
    try:
      with open("example.txt") as my_file:
          for line in myfile:
              print(line)
    except:
      print("There was an error when reading the file.")
  • An except block can hide the actual error: the problem here was not caused by file handling as such, but by the variable name which was misspelled. Without the except block the error thrown would be shown, and the cause could be found more easily. Therefore it is usually a good idea to use only except blocks specifically declared for certain error types.
  • Passing exceptions: If executing a function causes an exception, and this exception is not handled, it is passed on to the section of code which called the function, and so forth up the call chain, until it reaches the main function level. If it is not handled there, either, the execution of the program halts, and the exception is usually printed out for the user to see.
    
    def testing(x):
      print(int(x) + 1)
    
    try:
      number = input("Please type in a number: ")
      testing(number)
    except:
      print("Something went wrong")
    
    OutPut:
    Please type in a number: three
    Something went wrong
    
    
  • You can raise your own exceptions with a raise command.
    For example when detecting invalid parameters. Instead of printing an error which can be missed, we raise it to make debugging easier.
  • 
        def factorial(n):
        if n < 0:
            raise ValueError("The input was negative: " + str(n))
        k = 1
        for i in range(2, n + 1):
            k *= i
        return k
    
    print(factorial(3))
    print(factorial(6))
    print(factorial(-1))
    
    
    
    OutPut:
    6
    720
    Traceback (most recent call last):
    File "test.py", line 11, in 
    print(factorial(-1))
    File "test.py", line 3, in factorial
    raise ValueError("The input was negative: " + str(n))
    ValueError: The input was negative: -1
    
      
  • Use try when the line can fail because of something outside your function. When you are checking conditions yourself, raise exceptions by yourself without the need for try.
    If python might complain, catch it.
    If you might complain, raise it.
    -LLM
    Note: This part was really confusing to me so I took the help.
  • It's very practice to raise exception in one function and catch it in another function or even the main function.
  • 4. Local and global variables
  • Local variable is only for a defined section of the program while a global variable is for use with any section of the program.
  • Variables defined within the main function are global variables.
  • Main function as the section of code that do not fall within any other function.
  • A global variable can't be changed from within another function unless we use the term global.
    
    def testing():
        global x
        x = 3
        print(x)
    
    x = 5
    testing()
    print(x)
    
  • Don't use global variables as a way to bypass function parameters or return values.
  • Global variables: When we need to have a common "higher level information available to all functions of the program. However, passing data into and out of functions is best handles by arguments and return values.
  • When many functions can access and alter a variable directly(global), it becomes difficult to track the program's state, which can make the program unpredictable. This becomes even bigger trouble in large projects.
  • You can also separate the implicit main function into its own which in case will convert the earlier global variables into a local variables as follows:
    
      # your main function goes here
    def main():
        inputs = input_from_user(5)
        print_result(inputs)
        analysis_result = analyze(inputs)
    
        print(analysis_result)
    
    # run the main function
    main()
    
  • PART 7

    1. Modules

    PART 8

    PART 9

    1. Objects and references 2. Objects as attributes 3. Encapsulation 4. Scope of methods 5. Class attributes 6. More examples with classes

    PART 10

    1. Class hierarchies 2. Access modifiers 3. Object oriented programming techniques 4. Developing a larger application

    PART 11

    1. List comprehensions 2. More comprehensions 3. Recursion 4. More recursion examples

    PART 12

    1. Functions as arguments 2. Generators 3. Functional programming 4. Regular expressions

    PART 13

    1. Pygame 2. Animation 3. Events 4. More pygame techniques

    PART 14

    1. Game project 2. Robot and boxes 3. Finishing the game 4. Your own game