One reason to change
A class or module should have one responsibility, which in practice means one reason to change. A report class that parses a file, applies business rules and formats text has to change whenever any of the three does, and testing its rules means building a file first.
class Invoice:
def __init__(self, lines):
self.lines = lines
def total(self):
total = 0
for qty, price in self.lines:
total += qty * price
return total
def render(invoice):
return f"Total: {invoice.total():.2f}"
print(render(Invoice([(2, 1.5), (1, 4.0)])))Invoice knows the rules and render knows the layout. A new layout leaves Invoice alone.
Refactoring in small steps
Pin the current behaviour with tests, move one piece out, run the tests, and repeat. Every step leaves working code, so you can stop at any point.
Your task
Split SalesReport into parse_sales(csv_text) returning (region, amount) pairs, a class SalesTotals(rows) whose by_region() returns a dict of sums, and render_totals(totals) producing the old text. sales_report(csv_text) connects them and returns what SalesReport(csv_text).text() did.
The explanation opens here once every check passes.