Solving Linear and Quadratic Equations to Explore Core Python Concepts
This approach uses mathematical problem solving as a unifying thread to introduce foundational Python constructs—starting from basic syntax and progressing through functions, object-oriented design, error handling, and modularization.
Linear Equations in One Variable
from sympy import symbols, Eq, solve
x = symbols('x')
equation = Eq(3 * x - 7, 5)
solution = solve(equation, x)
print(f"Solution: {solution[0]}") # Output: 4
Systems of Linear Equations
from sympy import symbols, Eq, solve
x, y = symbols('x y')
eq1 = Eq(2 * x + y, 9)
eq2 = Eq(x - y, 3)
sol = solve((eq1, eq2), (x, y))
print(f"Solution set: {sol}") # Output: {x: 4, y: 1}
Quadratic Equations: Symbolic and Numeric Approaches
Using sympy for exact symbolic solutions:
import sympy as sp
x = sp.symbols('x')
expr = x**2 - 5*x + 6
roots = sp.solve(expr, x)
print(f"Roots: {roots}") # Output: [2, 3]
Manual numeric implementation with full discriminant handling:
import math
def compute_roots(a: float, b: float, c: float) -> list:
if a == 0:
raise ValueError("Coefficient 'a' must be non-zero for a quadratic equation.")
discriminant = b**2 - 4*a*c
if discriminant > 0:
sqrt_d = math.sqrt(discriminant)
r1 = (-b + sqrt_d) / (2*a)
r2 = (-b - sqrt_d) / (2*a)
return [r1, r2]
elif discriminant == 0:
r = -b / (2*a)
return [r]
else:
real = -b / (2*a)
imag = math.sqrt(-discriminant) / (2*a)
return [complex(real, imag), complex(real, -imag)]
print(compute_roots(1, -5, 6)) # [2.0, 3.0]
print(compute_roots(1, -2, 5)) # [(1+2j), (1-2j)]
Visualizing Quadratic Functions
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
# Define coefficients
a, b, c = 1, -5, 6
# Generate domain and compute range
x_vals = np.linspace(-1, 6, 200)
y_vals = a * x_vals**2 + b * x_vals + c
# Plot function
plt.figure(figsize=(8, 5))
plt.plot(x_vals, y_vals, label=f'$y = {a}x^2 + {b}x + {c}$', color='blue')
plt.axhline(0, color='gray', linewidth=0.8)
plt.axvline(0, color='gray', linewidth=0.8)
plt.grid(True, alpha=0.3)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Quadratic Function and Its Real Roots')
# Compute and mark roots
x_sym = sp.symbols('x')
expr_sym = a * x_sym**2 + b * x_sym + c
roots_sym = sp.solve(expr_sym, x_sym)
for root in roots_sym:
if root.is_real:
plt.scatter(float(root), 0, color='red', zorder=5)
plt.legend()
plt.show()
Core Language Features Demonstrated Through Equation Solving
Variables and Data Types
Python variables act as dynamic containers. Coefficients (a, b, c) are assigned numeric values—integers or floats—and the complex type handles imaginary components naturally.
Control Flow
Conditional logic determines root behavior based on the discriminant:
if discriminant > 0:
# Two distinct real roots
elif discriminant == 0:
# One repeated real root
else:
# Two complex conjugate roots
Functions
Encapsulating computation improves reusability and clarity:
def quadratic_solver(a: float, b: float, c: float) -> list:
"""Compute all roots of ax² + bx + c = 0."""
# ... implementation as above ...
return roots
Object-Oriented Design
A class-based solver separates concerns and supports extensibility:
class PolynomialSolver:
def __init__(self, coeffs: list):
self.coeffs = coeffs # descending powers: [a, b, c] for ax²+bx+c
def roots(self) -> list:
if len(self.coeffs) == 3:
a, b, c = self.coeffs
return compute_roots(a, b, c)
raise NotImplementedError("Only quadratic supported in this version.")
solver = PolynomialSolver([1, -5, 6])
print(solver.roots())
Error Handling
Robust code anticipatse invalid inputs:
def safe_quadratic_solver(a, b, c):
try:
return compute_roots(float(a), float(b), float(c))
except (ValueError, TypeError) as e:
print(f"Input error: {e}")
return []
except ZeroDivisionError:
print("Invalid coefficient: 'a' cannot be zero.")
return []
Modular Structure
Separating logic into modules promotes maintainability. A file algebra.py might contain:
# algebra.py
from typing import List, Union, Optional
def discriminant(a: float, b: float, c: float) -> float:
return b**2 - 4*a*c
def solve_quadratic(a: float, b: float, c: float) -> List[Union[float, complex]]:
# ... same logic ...
Then imported elsewhere:
from algebra import solve_quadratic
result = solve_quadratic(1, -5, 6)
File I/O Integration
Persisting results using context managers ensures safe rseource handling:
roots = solve_quadratic(1, -5, 6)
with open("solutions.txt", "w") as f:
f.write(f"Roots: {roots}\n")
# Later, read back
with open("solutions.txt", "r") as f:
print(f.read().strip())