(For context see A-level Computer Science)

This assignment was to write a basic income tax calculator using the simplest rules for personal allowance and tax rates at different income bands. The necessary information is advertised on the UK Government’s HMRC site and an online tax calculator was suggested for checking the results.

My son completed this at school with no input from me and came up with the following:

``````def TestInt(x):
try:
int(x)
except:
return -1
return x

def personal(y):
y = int(y)
if int(y) > 100000:
y -= 100000
if y % 2 == 0:
personal = 12570 - (y / 2)
return personal
else:
personal = 12570 - ((y - 1) / 2)
return personal
else:
return 12570

def taxamount(income):
allowance = personal(income)
if int(income) < 125140:
income = int(income) - allowance
else:
pass
tax = 0
output = 0
if (int(income) < 37700):
tax = 20
bound = 0
elif (37700 < int(income)) and (int(income) < 137430):
tax = 40
bound = 37700
elif int(income) > 137430:
tax = 45
bound = 137430
output = (((int(income) - int(bound)) / 100) * tax) + additional
if output < 0:
output = 0
return output

while True:
inc = int(-1)
while (int(inc) < 0):
inc = input("What are your total earnings for this year in GBP?")
inc = TestInt(inc)
tax = int(taxamount(inc))
TI = int(inc) - int(tax)
print("your tax is", tax, "and you keep", TI)
``````

We talked through this code and made several observations, including:

• The names of many things could be better. This lead to a discussion of the fact that the two hard problems in computing are naming things, cache invalidation and off-by-one errors. This in turn lead to an interesting but irrelevant derail concerning what exactly is meant by cache invalidation. We also talked about maintainability being more important than ‘correct’ functioning.
• There is some input validation, which is good to see at this early stage. We talked about why this is so important, in terms of both UX and security, with an entertaining diversion to Little Bobby Tables. The validation code might be improved by moving some more of the logic into the `TestInt` function.
• The personal allowance calculator could be simplified by simply dividing the amount above £100,000 by 2 and rounding, rather than the special cases for odd and even numbers we see here.
• The tax calculation is very much hard-coded and would be difficult to maintain if the tax bands changed, and even more difficult if the number of bands increased.
• There’s a lot of casting to `int` which could be better encapsulated.
• There are no unit tests! Of course, the idea of unit tests hasn’t been mentioned in this A-level course yet. I will be interested to see if it ever is. Meanwhile I couldn’t not mention it!

Aside from all this, the worst problem was that the results do not match those produced by the suggested online tax calculator. After a bit of research we narrowed this down to two issues:

• UK personal allowance (untaxed income) is based on a code, the numerical part of which is multiplied by 10 to produce an untaxed income allowance. Thus if your tax code is, say, 1257A then you may earn £12,570 before having to pay income tax. Since the multiplication by 10 means that the next possible level of allowance would be £12,580 there is obviously a gap of £10 between the possible levels. Unfortunately there seems to be some ambiguity about how rounding should be applied and thus whether the boundary between these bands effectively occurs at 12570, 12575 or 12579 (or elsewhere!). This leads to discrepancies between different online tax calculators.
• There is a further discrepancy between the suggested online calculator and our code, which we were unable to identify. Performing the calculations manually on the suggested test incomes produces outputs that match our code but not the online calculator, and the online calculator does not expose enough of its working for us to be able to see why that should be.

This was a useful talking point to illustrate that one of the biggest problems faced by software developers in the real world is understanding the client’s requirements, especially as on close investigation it often turns out that the clients themselves do not have an adequately precise understanding of their own requirements for them to be reliably encoded in software. This means that refining requirements must often be a collaboration between developers and their clients leading to a common understanding, which might be different from that of both parties at the beginning of the process.

After talking about these things, we went through the code and refactored several times before eventually settling on this:

``````bands = [
{"income":     50270, "tax": 20},
{"income":    137430, "tax": 40},
{"income": 999999999, "tax": 45}
]

def getAllowance(income):
allowance = 12570.0
if income > 100000:
allowance -= int((income - 100000) / 2)
if allowance < 0:
allowance = 0
return allowance

def taxOn(income):
allowance = getAllowance(income)
tax = 0.0
lastBreakPoint = allowance
for band in bands:
bandTop = band["income"]
if bandTop > income:
bandTop = income
if bandTop > lastBreakPoint:
tax += (bandTop - lastBreakPoint) * band["tax"] / 100
lastBreakPoint = bandTop
return tax

def getIntegerInput(prompt):
while True:
try:
return int(input(prompt + " "))
except:

def test():
tests = [
{"income":  10000, "tax":     0.00},
{"income":  32000, "tax":  3884.20},
{"income":  52000, "tax":  8232.00}, # https://www.moneyhelper.org.uk/en/work/employment/how-income-tax-and-personal-allowance-works
{"income":  52000, "tax":  8228.40}, # https://listentotaxman.com/52000?
{"income":  65000, "tax": 13428.40},
{"income": 106000, "tax": 31028.40},
{"income": 132500, "tax": 45460.00},
{"income": 175000, "tax": 63710.00}
]

for test in tests:
income = test["income"]
expectedTax = test["tax"]

tax = taxOn(income)

if (abs(tax - expectedTax) <= 2):
result = f"pass - tax = {tax}"
else:
result = f"FAIL - expected {expectedTax} but got {tax}"

print("Test income", income, ":", result)

def main():
while True:
income = getIntegerInput("What are your total earnings for this year in GBP?")
if income <= 0:
break
tax = taxOn(income)
net = income - tax
print("Your tax is", tax, "and you keep", net)

test()
main()
``````

Merlyn Kline
October 2021