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.
Topic map
- Algorithms, pseudocode and Python execution
- Names, assignment, types and objects
- Input, conversion and formatted output
- Arithmetic, precedence and floating-point limits
- Comparison, logic and truth values
- Selection, indentation and boundary values
- for loops, range and accumulators
- while loops, break, continue and loop else
- Strings, indexing and useful methods
- List basics, indexes and slices
- Mutation, aliases and shallow copies
- List methods, sorting and comprehensions
- Tuples, unpacking and sets
- Dictionaries and word frequency
- Functions, parameters and returned values
- Argument forms and mutable-default pitfalls
- Scope, lambda and recursion
- Modules, imports and script entry points
- Exceptions, else, finally and meaningful errors
- Text files, encodings and context managers
- CSV, JSON and structured data
- Classes, instances and initialization
- Inheritance, composition and polymorphism
- Iterators and generators
- Searching, sorting and simple complexity
- Prime testing and boundary handling
- Maximum, empty collections and contracts
- Testing, debugging and safe execution
- Nested loops and matrix-shaped lists
- NumPy arrays, shape, dtype and broadcasting
- Special methods and operator overloading
- 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.
Answer: A syntax error prevents parsing. A logic error can run successfully while producing an incorrect result.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Answer: No. A tuple cannot replace its entries, but an entry can reference a mutable 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.
index: 0
negative: -3
index: 1
negative: -2
index: 2
negative: -1
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
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.
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 pair | Meaning | Syntax | Check |
|---|---|---|---|
| list | Ordered, mutable | [] | A shallow copy shares nested objects |
| tuple | Ordered, immutable entries | (1,) | Nested objects can remain mutable |
| set | Unique hashable elements | set() | {} creates an empty dictionary |
| dict | Key-value mapping | {} | Keys must be hashable |
| str | Immutable Unicode sequence | "" | A character may not be a visible grapheme |
| return / print | Caller value / display | return 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
- State input types, bounds and empty-input behavior.
- Keep indentation and string escapes intact when copying.
- Test normal, boundary and error branches with expected outputs.
- Do not mutate shared data accidentally or rely on a mutable default.
- Use safe practice paths and close files with a context manager.
- Document required libraries and interpreter assumptions.
Comments
Post a Comment