⚡ TechVani: AI, Technology, Apps, Coding & Digital World
TechVaniAI • Technology • Apps • Coding • Digital World
HomeAI & TechAppsCodingFree AI ToolsStudent TechSecurityAboutContactPrivacy
TECHVANI // LEARN. BUILD. TEST.

Learn the tech. Build the idea.

Clear study notes, tested lab files and practical guides for coding, AI and digital safety.

Explore Latest →
šŸ¤– AI & TechAI news, tools & guides šŸ“± Apps & ToolsUseful apps & websites šŸ’» CodingProgramming & web development šŸŽ“ Student TechStudy & career technology šŸ” Cyber SecuritySafety & awareness

Latest Posts

Circuit Notebook theme

Exam Notes

šŸ“š BTech CSE Exam Notes

Find Semester 1-8 subjects in one place. Choose a semester and open the notes you need. This is a general study guide, not an official syllabus.

Open Exam Notes →🧪 Open Lab Files →

Exam Numericals & Study Series

Practice CPU scheduling and explore OS and DBMS topics in separate, easy-to-follow parts. New topics will appear in the hub as they are published.

Open Numericals Hub →

Start here and stay in touch

New to TechVani? Start with our free AI tools guide for students. It covers which tools to try and what to check before relying on an answer.

For step-by-step videos, visit SiteSudhar on YouTube.

Join TechVani on WhatsApp

Get new AI tools and coding guides in your WhatsApp Updates tab.

Browse Topics

Popular Posts

TechVani

Introduction to Python Programming | Semester 2 BTech CSE Notes

Semester 2 - Page 3: Introduction to Python Programming. A general engineering revision guide, not an official syllabus or a past-paper prediction. Placement and assessed libraries vary. Core examples use Python 3 syntax and were executed with Python 3.10.12; the separately labelled NumPy extension was tested with NumPy 2.2.6 and needs NumPy installed. Run each block on its own.

Study rule: describe the input contract, predict the output, run the program, then test empty and boundary cases. Printing is not returning, assignment is not copying and one passing test is not a proof.

Topic map

  1. Algorithms, pseudocode and Python execution
  2. Names, assignment, types and objects
  3. Input, conversion and formatted output
  4. Arithmetic, precedence and floating-point limits
  5. Comparison, logic and truth values
  6. Selection, indentation and boundary values
  7. for loops, range and accumulators
  8. while loops, break, continue and loop else
  9. Strings, indexing and useful methods
  10. List basics, indexes and slices
  11. Mutation, aliases and shallow copies
  12. List methods, sorting and comprehensions
  13. Tuples, unpacking and sets
  14. Dictionaries and word frequency
  15. Functions, parameters and returned values
  16. Argument forms and mutable-default pitfalls
  17. Scope, lambda and recursion
  18. Modules, imports and script entry points
  19. Exceptions, else, finally and meaningful errors
  20. Text files, encodings and context managers
  21. CSV, JSON and structured data
  22. Classes, instances and initialization
  23. Inheritance, composition and polymorphism
  24. Iterators and generators
  25. Searching, sorting and simple complexity
  26. Prime testing and boundary handling
  27. Maximum, empty collections and contracts
  28. Testing, debugging and safe execution
  29. Nested loops and matrix-shaped lists
  30. NumPy arrays, shape, dtype and broadcasting
  31. Special methods and operator overloading
  32. Problem-solving strategies and iterative sequences

1. Algorithms, pseudocode and Python execution

An algorithm is a finite sequence of steps for a defined task. State its inputs, outputs, assumptions and stopping condition before translating it into Python. Pseudocode describes logic without requiring exact language syntax; a flowchart uses standard shapes and branches. Running a program is a check, not a proof that every input is correct.

Worked example: To average marks: reject empty data, add the marks, divide by their count, then return the result. The empty-data decision belongs in the algorithm, not as an accidental division error.
Exam question: What is the difference between syntax and logic errors?
Answer: A syntax error prevents parsing. A logic error can run successfully while producing an incorrect result.
Input, output and error contractAlgorithm and stopping rulePython code and readable namesExpected output and boundary tests
Work from a clear contract to tested code. A failed test sends you back to the algorithm or implementation.

2. Names, assignment, types and objects

A name is bound to an object by assignment. Python objects have types; a name can later be rebound to a different object. Integers have arbitrary precision subject to memory limits; floats usually use finite binary precision. Common types include int, float, bool, str and NoneType. = assigns; == compares values; is tests identity. Use meaningful names and do not overwrite built-ins such as list or sum.

Worked example: total=10 binds a name to an integer. total=total+2 computes a new integer and rebinds the name. This is different from changing a mutable list in place.
Exam question: Is a variable declaration with a fixed type required?
Answer: No. Python determines the object type at runtime, though optional type annotations can describe intended use.

3. Input, conversion and formatted output

input() returns a string without the final line terminator. Convert only when a numeric value is required and handle a failed conversion. print() displays values; an f-string can interpolate and format them. Formatting a float for display does not change its stored value. Never use eval() to parse ordinary untrusted numeric input.

Worked example: Convert the text "42" with int; convert decimal text with float. A value of 2.5 formatted with .2f displays 2.50. Invalid integer text raises ValueError.
Exam question: Why does input()+input() concatenate rather than add numbers?
Answer: Both values are strings. Convert validated inputs to the appropriate numeric type before numeric addition.

Run this block on its own

raw = "42"
number = int(raw)
print(number + 8)
print(f"{5 / 2:.2f}")
try:
    int("forty")
except ValueError:
    print("Invalid integer")

Checked output:

50
2.50
Invalid integer

4. Arithmetic, precedence and floating-point limits

/ performs true division, // performs floor division, % gives the remainder under Python rules, and ** raises a power. Parentheses make intended grouping clear. Floor division rounds toward minus infinity, not toward zero. Decimal fractions often lack exact binary representation; use tolerances for approximate comparisons and decimal arithmetic when an application needs specified decimal behavior.

Worked example: −5//2 is −3 and −5%2 is 1, satisfying a=(a//b)b+a%b. In a float calculation, 0.1+0.2 need not compare exactly equal to 0.3.
Exam question: Does // always return an int?
Answer: No. With float operands, the floor-division result is a float, such as 5.0//2.0 giving 2.0.

Run this block on its own

import math
print(5 / 2, 5 // 2, 5 % 2)
print(-5 // 2, -5 % 2)
print(0.1 + 0.2 == 0.3)
print(math.isclose(0.1 + 0.2, 0.3))

Checked output:

2.5 2 1
-3 1
False
True

5. Comparison, logic and truth values

Comparisons produce boolean results; chained comparisons such as 0<=mark<=100 express a range check. Empty containers, zero, None and False are false in boolean contexts. and and or short-circuit and return selected operands, not necessarily bool objects. not produces a boolean. Use == for value comparison and is None for the singleton None.

Worked example: A condition values and values[0]>0 avoids indexing an empty list because the second operand is skipped when values is empty.
Exam question: What does "" or "default" return?
Answer: The string "default", not True. The first operand is false, so or returns the second operand.

6. Selection, indentation and boundary values

if, elif and else choose branches. A colon starts the suite, and consistent indentation defines its body. Test thresholds from both sides; one successful middle value does not validate a range boundary. A ternary expression chooses one value, while a multi-branch if statement is usually clearer for several actions. Do not use assignment where comparison was intended.

Worked example: A passing threshold of 40 needs tests at 39 and 40. A valid-mark range of 0 through 100 also needs 0, 100 and out-of-range tests.
Exam question: Why is indentation more than formatting in Python?
Answer: It determines block structure. Moving a statement into or out of a block changes its execution or can cause an indentation error.

Run this block on its own

def grade(mark):
    if not 0 <= mark <= 100:
        raise ValueError("marks must be 0 to 100")
    return "pass" if mark >= 40 else "fail"

print([grade(x) for x in (0, 39, 40, 100)])
try:
    grade(101)
except ValueError as error:
    print(error)

Checked output:

['fail', 'fail', 'pass', 'pass']
marks must be 0 to 100

7. for loops, range and accumulators

A for loop visits items from an iterable. range(start, stop, step) excludes stop, and a negative step can count downward. Initialize an accumulator before the loop. enumerate gives index-value pairs; zip pairs iterables and by default stops at the shortest. Avoid changing a collection structurally while iterating over it unless the behavior is deliberately managed.

Worked example: Count marks meeting a threshold by incrementing passed inside the condition. Empty input should leave the initial count at zero.
Exam question: What values does range(1,6,2) generate?
Answer: 1, 3 and 5. It stops before 6.

Run this block on its own

marks = [72, 85, 68]
passed = 0
for mark in marks:
    if mark >= 40:
        passed += 1
print(passed)
print(list(range(1, 6, 2)))

Checked output:

3
[1, 3, 5]

8. while loops, break, continue and loop else

A while loop repeats while its condition is true; establish an update or another termination mechanism. break leaves the nearest loop, and continue skips the remainder of the current iteration. A loop else runs when the loop finishes without break, including an initially empty iteration. It is not equivalent to the else branch of an if inside the loop.

Worked example: A search loop can break when it finds a match and use loop else for "not found". A while loop that never changes its controlling state can run forever.
Exam question: Does continue prevent the loop else from running?
Answer: No. A completed loop with continue but no break still reaches its else.

Run this block on its own

for value in [2, 4, 6]:
    if value % 2 == 1:
        print("Odd found")
        break
else:
    print("No odd value")

Checked output:

No odd value

9. Strings, indexing and useful methods

A string is an immutable sequence of Unicode characters. Indexes start at zero; negative indexes count from the end. str methods such as lower, strip, split and join return results rather than changing the original string. A slice uses start:stop:step and excludes stop. A Python character is not guaranteed to be a complete visible grapheme in every writing system.

Worked example: "Python"[::-1] produces "nohtyP". " a,b ".strip().split(",") produces two fields for this simple unquoted case, but it is not a general CSV parser.
Exam question: Can text[0]="X" modify a string?
Answer: No. String item assignment raises TypeError. Build a new string instead.

Run this block on its own

text = "Python"
print(text[0], text[-1])
print(text[1:4])
print(text[::-1])
print("-".join(["learn", "test"]))

Checked output:

P n
yth
nohtyP
learn-test

10. List basics, indexes and slices

A list is an ordered mutable sequence that can hold references to different objects. For length n, nonnegative indexes run from 0 to n−1; an empty list has no valid item index. A slice creates a new outer list and clips ordinary oversized bounds. append adds one object and returns None; extend visits an iterable and adds its elements. The detailed Proof Lab below preserves the earlier list experiments.

Worked example: [10,20,30,40][1:3] gives [20,30]. A request for item [4] on that list raises IndexError, while [:99] safely returns all its items.
Exam question: Does append return the changed list?
Answer: No. It changes the list in place and returns None; assigning that return to the list name loses that binding.

11. Mutation, aliases and shallow copies

Assignment such as b=a creates another binding to the same list, not a new list. a.copy() or a[:] makes a shallow outer copy; nested mutable objects are still shared. A deep copy may be needed for nested structures, but copying has cost and object-specific behavior. Distinguish rebinding a name from mutating the shared object.

Worked example: Appending through an alias affects the original list. With [[1],[2]], a shallow copy shares both inner lists, so appending to copied[0] also changes original[0].
Exam question: Does tuple immutability make every nested object immutable?
Answer: No. A tuple cannot replace its entries, but an entry can reference a mutable list.
originalaliasOne list object[[1], [2]]Assignment adds a name, not a copy
Aliasing sketch. Mutation through either name changes the same outer list.

Run this block on its own

original = [[1], [2]]
alias = original
copy = original.copy()
copy[0].append(9)
print(alias is original)
print(copy is original)
print(original)

Checked output:

True
False
[[1, 9], [2]]

12. List methods, sorting and comprehensions

remove deletes the first matching value; pop removes and returns an item; del can remove an indexed range. list.sort changes a list in place and returns None, while sorted creates a new list from an iterable. A comprehension expresses a transformation with an optional filter. Prefer a readable loop when the expression becomes difficult to explain.

Worked example: [x*x for x in range(1,6) if x%2==1] gives [1,9,25]. Calling sorted does not change the original list.
Exam question: What is the difference between append([3,4]) and extend([3,4])?
Answer: append adds one nested list object; extend adds 3 and 4 as two elements.

Run this block on its own

values = [3, 1, 2]
print(sorted(values))
print(values)
print(values.sort())
print(values)
print([x*x for x in range(1, 6) if x % 2 == 1])

Checked output:

[1, 2, 3]
[3, 1, 2]
None
[1, 2, 3]
[1, 9, 25]

13. Tuples, unpacking and sets

A tuple is an ordered immutable sequence; a one-item tuple needs a comma. Unpacking assigns components to names, with a starred target able to collect remaining items. A set stores distinct hashable elements and supports union, intersection and difference. Set iteration order is not a stable sorted output contract; sort it before displaying ordered examples.

Worked example: (5,) is a tuple but (5) is just an integer expression. {1,1,2} has two distinct elements.
Exam question: Can a list be a set element?
Answer: No. Lists are unhashable. A suitable tuple of hashable elements can be a set element.

Run this block on its own

first, *rest = (10, 20, 30)
print(first, rest)
print(sorted({1, 2} | {2, 3}))
print(sorted({1, 2} & {2, 3}))

Checked output:

10 [20, 30]
[1, 2, 3]
[2]

14. Dictionaries and word frequency

A dictionary maps unique hashable keys to values. Accessing a missing key with [] raises KeyError; get can supply a default. items provides key-value pairs. Current Python dictionaries preserve insertion order, but that is different from sorting by key. Define text-normalization rules before counting words; whitespace splitting plus edge punctuation stripping is only a simple tokenizer.

Worked example: For "AI ai tools, tools help AI", lowercasing and stripping comma/period at word edges gives counts ai:3, tools:2, help:1. Apostrophes, Unicode normalization and internal punctuation need a richer policy if required.
Exam question: Why use freq.get(word,0)+1?
Answer: It handles the first occurrence without a missing-key error and increments later occurrences.

Run this block on its own

text = "AI ai tools, tools help AI"
words = [word.strip(",.").lower() for word in text.split()]
freq = {}
for word in words:
    freq[word] = freq.get(word, 0) + 1
print(freq)

Checked output:

{'ai': 3, 'tools': 2, 'help': 1}

15. Functions, parameters and returned values

Define a function with def, parameter names and an indented body. Arguments supply values at the call site. return sends a value to the caller and ends that call; falling off the end returns None. A print inside the function displays something but does not substitute for return. Write a clear input contract and decide how invalid or empty data is handled.

Worked example: An average function should reject an empty list explicitly instead of dividing by zero. It can return a numeric mean for nonempty numeric input.
Exam question: Why can print(average(...)) show None after a number?
Answer: If average only prints rather than returns, the outer print displays its implicit None result.

Run this block on its own

def average(values):
    if not values:
        raise ValueError("no values")
    return sum(values) / len(values)

print(average([60, 70, 80]))
try:
    average([])
except ValueError as error:
    print(error)

Checked output:

70.0
no values

16. Argument forms and mutable-default pitfalls

Functions can accept positional and keyword arguments; defaults are evaluated when the definition executes, not afresh at every call. A mutable list default can therefore retain changes between calls. Use None as a sentinel and create the list inside when a fresh value is intended. *args collects extra positional arguments into a tuple; **kwargs collects extra keywords into a dictionary.

Worked example: def collect(item,bucket=None) can set bucket=[] only when bucket is None. Using if not bucket would also replace an explicitly supplied empty list, which may not be intended.
Exam question: Are two calls guaranteed a fresh list with def f(items=[])?
Answer: No. That default list is shared across calls unless explicitly replaced.

Run this block on its own

def collect(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(collect("a"))
print(collect("b"))

Checked output:

['a']
['b']

17. Scope, lambda and recursion

Names are resolved through local, enclosing, global and built-in scopes in the common model. Assignment inside a function normally makes a name local unless declared otherwise. A lambda is a small expression-based function, not a requirement for every callback. Recursion needs a base case and progress toward it; deep calls can hit the recursion limit, and repeated recursive work can be expensive.

Worked example: A factorial recurrence is n!=n×(n−1)! with 0!=1. Restrict the input to nonnegative integers; a loop often avoids recursion-depth concerns.
Exam question: Why is factorial of a negative integer rejected here?
Answer: The stated elementary factorial contract is for nonnegative integers. Recursive decrementing from a negative value would not reach zero.

Run this block on its own

def factorial(n):
    if not isinstance(n, int) or n < 0:
        raise ValueError("nonnegative integer required")
    if n == 0:
        return 1
    return n * factorial(n - 1)

print(factorial(0), factorial(5))

Checked output:

1 120

18. Modules, imports and script entry points

A module is a file of Python definitions and statements; import loads it and binds an accessible name. Use qualified names or selected imports rather than wildcard imports that obscure origins. A package organizes modules. if __name__=="__main__" separates direct script execution from imported helper behavior. Do not name your file after a library you need, such as random.py.

Worked example: import math followed by math.sqrt(81) returns 9.0. A main guard lets a file expose tested functions without automatically running interactive prompts on import.
Exam question: Is importing a module always side-effect free?
Answer: No. Its top-level statements normally execute on first import in a process. Review modules and avoid untrusted code.

19. Exceptions, else, finally and meaningful errors

try guards an operation; except handles specified exception types. else runs when the try block completes without an exception, and finally runs when leaving the construct under ordinary execution, whether an error occurred or not. Catch the smallest relevant types and keep the protected block focused. Do not silently swallow failures or return from finally in a way that hides an earlier result or exception.

Worked example: Catch ValueError around a numeric conversion and show an input message. FileNotFoundError is more appropriate for a missing file than a blanket except.
Exam question: Does finally mean the process can never skip it?
Answer: No. Abrupt process termination can prevent cleanup. It is a language control-flow guarantee for normal execution paths, not a power-failure guarantee.

Run this block on its own

try:
    value = int("12")
except ValueError:
    print("Invalid")
else:
    print(value * 2)
finally:
    print("Cleanup")

Checked output:

24
Cleanup

20. Text files, encodings and context managers

Use with open(...,encoding="utf-8") for text files when that encoding matches the data. r reads, w truncates or creates, a appends; binary modes operate on bytes. A with block closes the file even when its body raises an ordinary exception. Reading all lines can consume large memory; iterate over a file for large inputs. Only use approved paths and never overwrite an important file during a practice example.

Worked example: The example below uses a temporary directory, writes a small UTF-8 file and reads it back. It does not need a preexisting scores.txt or leave a file in your working folder.
Exam question: Why is mode w dangerous on an existing file?
Answer: It truncates the file on opening. Use it only when replacing that content is intended.

Run this block on its own

from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as folder:
    path = Path(folder) / "scores.txt"
    with path.open("w", encoding="utf-8") as file:
        file.write("60\n70\n80\n")
    with path.open(encoding="utf-8") as file:
        scores = [int(line.strip()) for line in file]
    print(scores)
    print(sum(scores) / len(scores))

Checked output:

[60, 70, 80]
70.0

21. CSV, JSON and structured data

Use csv for quoted tabular fields and json for JSON objects rather than inventing a parser from split. JSON arrays become lists and objects become dictionaries; JSON null becomes None. Validate structure and types after parsing. Never load untrusted pickle data because deserialization can execute code. A parsed document is not automatically a trustworthy claim or instruction.

Worked example: A CSV row can contain a comma inside a quoted name; split(",") would separate it incorrectly. JSON can round-trip a simple string-key dictionary and numeric list.
Exam question: Does json.loads prove the data matches your application schema?
Answer: No. It proves syntactic parsing, not that required keys, types, bounds or authorship are valid.

Run this block on its own

import csv
import io
import json

row = next(csv.reader(io.StringIO('"Ravi, Kumar",82\n')))
print(row)
value = json.loads('{"marks": [60, 70]}')
print(value["marks"])

Checked output:

['Ravi, Kumar', '82']
[60, 70]

22. Classes, instances and initialization

A class defines behavior and attributes for instances. __init__ initializes a newly created instance; self names the receiving instance by convention. Store per-instance mutable data on self rather than unintentionally sharing it as a class attribute. A method is a function associated with the class whose instance argument is supplied on a bound call. Avoid using inheritance where a small plain function is clearer.

Worked example: A Counter instance can store its own value and expose an increment method. Two instances should not accidentally share the same changing count.
Exam question: Is __init__ the operation that creates the instance object?
Answer: Strictly, creation is handled by __new__; __init__ initializes the created instance. Introductory examples usually only customize __init__.

Run this block on its own

class Counter:
    def __init__(self, start=0):
        self.value = start

    def increment(self):
        self.value += 1
        return self.value

first = Counter()
second = Counter(10)
print(first.increment(), second.increment())

Checked output:

1 11

23. Inheritance, composition and polymorphism

Inheritance can specialize a base class; overriding supplies a subclass method with the same role. Composition stores one useful object inside another. Python code often uses duck typing: it calls the required operations rather than insisting on a particular concrete class. Encapsulation is supported by conventions and properties, not an absolute private-memory wall created by an underscore.

Worked example: A report object can contain a formatter rather than inherit from every possible formatter. Both can be tested through the operation they expose, such as render().
Exam question: What does a leading underscore in _cache usually mean?
Answer: It signals an internal-use convention. It does not by itself prevent outside access.

24. Iterators and generators

An iterable can supply an iterator; an iterator advances through next() and raises StopIteration when finished. A generator function uses yield and resumes its state on later requests. Generators can avoid storing a whole sequence at once, but they are usually one-pass objects. Laziness changes when work and exceptions occur, so test the consumption step too.

Worked example: A generator of squares yields 1,4,9 as consumed. After it is exhausted, converting it to a list again returns an empty list.
Exam question: Does making a generator execute its whole body immediately?
Answer: No. Its body normally begins when the generator is advanced.

Run this block on its own

def squares(limit):
    for number in range(1, limit + 1):
        yield number * number

values = squares(3)
print(list(values))
print(list(values))

Checked output:

[1, 4, 9]
[]

25. Searching, sorting and simple complexity

Linear search checks items until it finds a match or exhausts the data, requiring O(n) comparisons in the worst case. Binary search needs sorted data and halves the search interval, taking O(log n) comparisons in the elementary model. Sorting first has its own cost. Big-O describes growth under assumptions, not the exact seconds a particular machine takes.

Worked example: Searching an unsorted list with binary search is invalid without first establishing order. For one tiny search, sorting solely to use binary search may cost more than a linear scan.
Exam question: Why is O(log n) not enough to call a complete workflow faster?
Answer: Preprocessing, data size, repeated queries and operation costs also matter. State what part of the workflow is being analyzed.

26. Prime testing and boundary handling

A prime is an integer at least two with no positive divisors other than one and itself. If a composite number has no divisor below or at its square root, its factor pair would be impossible, so test while divisor²≤n. Python integer multiplication does not have the fixed-width overflow issue of a C int, but very large inputs still cost time.

Worked example: For integer inputs, the checker returns False for negative values, zero and one, True for two, and detects a perfect-square composite such as 49.
Exam question: Why must the condition include equality?
Answer: A square such as 49 has its decisive divisor exactly at √49=7. Using only < would miss it.

Run this block on its own

def is_prime(number):
    if number < 2:
        return False
    divisor = 2
    while divisor * divisor <= number:
        if number % divisor == 0:
            return False
        divisor += 1
    return True

print([n for n in range(1, 21) if is_prime(n)])
print([is_prime(n) for n in (-1, 0, 1, 2, 49)])

Checked output:

[2, 3, 5, 7, 11, 13, 17, 19]
[False, False, False, True, False]

27. Maximum, empty collections and contracts

An algorithm must define empty-input behavior before reading the first item. A manual maximum can obtain an iterator, read its first value or raise a clear error, then compare the rest. This avoids choosing zero as an incorrect default when every input is negative. State that items must be comparable under the intended ordering.

Worked example: For [−8,−3,−10], the maximum is −3, not zero. An empty iterable is rejected explicitly in the example.
Exam question: Why not initialize the maximum to zero?
Answer: Zero might not appear in the input and is greater than every value in an all-negative list.

Run this block on its own

def maximum(values):
    iterator = iter(values)
    try:
        best = next(iterator)
    except StopIteration:
        raise ValueError("no values") from None
    for value in iterator:
        if value > best:
            best = value
    return best

print(maximum([-8, -3, -10]))
try:
    maximum([])
except ValueError as error:
    print(error)

Checked output:

-3
no values

28. Testing, debugging and safe execution

Write expected outcomes before running a test. Cover normal cases, empty input, boundaries, invalid input and type assumptions. assert is useful for internal checks, but optimized execution can remove assertions, so do not use it as the only validation of external data. Read tracebacks from the error type and relevant source line. Never execute copied untrusted code without reviewing its file, network and process effects.

Worked example: A passing grade example at 50 does not test 39/40, 0/100, 101, negative input or conversion failure. Separate input parsing from grade logic so each can be tested.
Exam question: Does passing a few tests prove a program has no bugs?
Answer: No. It supplies evidence for tested cases. Clear contracts, review and wider tests are still needed.

29. Nested loops and matrix-shaped lists

A nested loop repeats the inner loop for each outer item. A list of lists can represent rows, but each row should be a distinct list when independent mutation is intended. [[0]*cols]*rows repeats references to the same row; a comprehension creates separate rows. Check dimensions before elementwise operations or multiplication. Python lists do not automatically behave like numerical matrices.

Worked example: Build a 2×3 zero table using [[0 for _ in range(3)] for _ in range(2)]. Updating one cell then leaves the other row unchanged.
Exam question: Why can changing one cell affect several rows with repeated-list construction?
Answer: Those rows are aliases of the same inner list, not separate copies.

Run this block on its own

matrix = [[0 for _ in range(3)] for _ in range(2)]
matrix[0][1] = 5
print(matrix)

Checked output:

[[0, 5, 0], [0, 0, 0]]

30. NumPy arrays, shape, dtype and broadcasting

NumPy is an optional third-party numerical library, not part of the Python standard library. An ndarray has a shape and a dtype, usually storing elements with a common representation. Slices often produce views that share data; use copy when independence is intended. Broadcasting aligns trailing dimensions where sizes match or one is 1. Fixed-width integer dtypes can overflow unlike Python integers.

Worked example: A 2×3 array plus a length-3 row broadcasts across both rows. Boolean masking selects elements matching a condition; elementwise * and matrix @ are different operations.
Exam question: What is the difference between array shape and dtype?
Answer: Shape states dimensions such as (2,3); dtype states the representation such as int64. Both matter for valid operations.

Run this block on its own

import numpy as np

array = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int64)
print(array.shape)
print((array + np.array([10, 20, 30])).tolist())
print(array[array > 3].tolist())
view = array[:, 1:]
view[0, 0] = 99
print(array.tolist())

Checked output:

(2, 3)
[[11, 22, 33], [14, 25, 36]]
[4, 5, 6]
[[1, 99, 3], [4, 5, 6]]

31. Special methods and operator overloading

Special methods define how an object participates in operations, such as __str__ for a readable string and __add__ for addition. A binary operator method should return NotImplemented when it cannot handle the other operand, allowing Python to try the alternative operation. This is different from raising NotImplementedError. Keep overloaded operations consistent with the meaning users expect.

Worked example: A two-coordinate value can add another compatible value component by component and format itself as (x, y).
Exam question: Does a method named __add__ have to change the original object?
Answer: No. It commonly returns a new result. Mutation is a separate design choice and should be documented.

Run this block on its own

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return Point(self.x + other.x, self.y + other.y)

    def __str__(self):
        return f"({self.x}, {self.y})"

print(Point(1, 2) + Point(3, 4))

Checked output:

(4, 6)

32. Problem-solving strategies and iterative sequences

Brute force systematically checks candidates; divide-and-conquer splits a problem into smaller parts; backtracking tries a choice and retracts it when constraints fail. A heuristic can guide a search without guaranteeing the best result. Dynamic programming reuses solved subproblems under an appropriate recurrence. A stopping rule must be justified or bounded: the general Collatz termination conjecture is not proved.

Worked example: A Fibonacci generator uses the pair update a,b=b,a+b. For an exploratory Collatz program, impose a step cap and report if it is reached rather than claiming every positive integer is known to terminate.
Exam question: Does a good heuristic guarantee an optimal answer?
Answer: No. Its quality depends on the problem and evidence. An exact algorithm or additional argument is needed for an optimality guarantee.

Run this block on its own

def fibonacci(count):
    if not isinstance(count, int) or count < 0:
        raise ValueError("nonnegative integer required")
    result = []
    a, b = 0, 1
    for _ in range(count):
        result.append(a)
        a, b = b, a + b
    return result

print(fibonacci(0))
print(fibonacci(7))

Checked output:

[]
[0, 1, 1, 2, 3, 5, 8]

List basics: preserved Proof Lab

Start here if lists are new to you. Watch the Python list Short, then run each experiment separately in Python 3. The outputs below were executed and checked.

What is a Python list?

Definition: a list is an ordered, mutable sequence. Ordered means items have positions. Mutable means the list can be changed after it is created. Write a list with square brackets and separate its items with commas.
fruits = ["apple", "banana", "mango"]

fruits is the variable name. The square brackets contain three strings. A string is text written inside quotes. A list can also hold numbers or other objects. Here we keep the examples simple by using fruit names.

Indexing: the first position is 0

An index identifies one item. Use square brackets after the list name, such as fruits[0]. A three-item list has positive indexes 0, 1 and 2, not 1, 2 and 3.

apple
index: 0
negative: -3
banana
index: 1
negative: -2
mango
index: 2
negative: -1
Read positive indexes from the left. Read negative indexes from the right.

Proof: read a list from zero

Run this block on its own:

fruits = ["apple", "banana", "mango"]
print(fruits[0])
print(fruits[1])
print(len(fruits))

Checked output:

apple
banana
3

len(fruits) counts the items. Length 3 does not make 3 a valid positive index. For a non-empty list of length n, positive indexes run from 0 through n - 1.

Negative indexing: -1 means the last item

Negative indexes start at the right end. -1 means the last item, -2 means the item before it, and so on. It is useful when you want the latest entry without first calculating the length.

Proof: count from the right

Run this block on its own:

fruits = ["apple", "banana", "mango"]
print(fruits[-1])
print(fruits[-2])

Checked output:

mango
banana

There is no special "negative zero" position: -0 equals 0, so it still selects the first item.

append(): add one item at the end

append() changes the existing list by adding one item after its current last item. It returns None, not the updated list. None is Python's value for no result here.

Proof: append one item

Run this block on its own:

fruits = ["apple", "banana", "mango"]
result = fruits.append("orange")
print(fruits)
print(result)

Checked output:

['apple', 'banana', 'mango', 'orange']
None
Common mistake: do not write fruits = fruits.append("orange"). That assigns the returned None to fruits. Call fruits.append("orange") on its own, then read fruits.

Slicing: include start, exclude end

A slice selects a range and returns a new list. The form is items[start:end]. For the ordinary forward slices here, start is included and end is excluded. fruits[1:3] therefore selects indexes 1 and 2. It stops before index 3.

Read it aloud: "start at 1, stop before 3". Do not read it as "items 1 to 3 inclusive".

Proof: stop before the end index

Run this block on its own:

fruits = ["apple", "banana", "mango", "orange"]
print(fruits[1:3])
print(fruits[:2])
print(fruits[2:])
print(fruits[-2:])

Checked output:

['banana', 'mango']
['apple', 'banana']
['mango', 'orange']
['mango', 'orange']

Leaving out the start means begin at the front. Leaving out the end means continue to the end. Negative indexes can also be slice boundaries: [-2:] gets the final two items of this four-item list.

Out-of-range indexing is different from slicing

Asking for one missing item raises IndexError. An oversized slice endpoint is clipped to the available list. A slice with equal start and end is empty.

Proof: index error versus safe slice

Run this block on its own:

fruits = ["apple", "banana", "mango"]
try:
    print(fruits[3])
except IndexError:
    print("IndexError")
print(fruits[1:99])
print(fruits[2:2])

Checked output:

IndexError
['banana', 'mango']
[]

The try/except block catches the error so the remaining lines still run. It prints only the error name; an uncaught error would instead show a traceback and stop that program.

Important differences

Type or pairMeaningSyntaxCheck
listOrdered, mutable[]A shallow copy shares nested objects
tupleOrdered, immutable entries(1,)Nested objects can remain mutable
setUnique hashable elementsset(){} creates an empty dictionary
dictKey-value mapping{}Keys must be hashable
strImmutable Unicode sequence""A character may not be a visible grapheme
return / printCaller value / displayreturn value / print(value)A function without return yields None

Practice route

Use the Python Programming Lab File and Lab Files index. In the Online Compiler, choose Python and paste one complete block. NumPy may not be installed there. For exceptions, also read the tested try/except/else/finally guide.

Final check before submission

  1. State input types, bounds and empty-input behavior.
  2. Keep indentation and string escapes intact when copying.
  3. Test normal, boundary and error branches with expected outputs.
  4. Do not mutate shared data accidentally or rely on a mutable default.
  5. Use safe practice paths and close files with a context manager.
  6. Document required libraries and interpreter assumptions.

Further study

Comments