Count Working Days in JavaScript With UK Bank Holidays
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
To read a CSV file in Python without pandas, use the built-in csv module: open the file with open("file.csv", newline="", encoding="utf-8-sig"), then loop over csv.reader(f) to get each row as a list, or csv.DictReader(f) to get each row as a dictionary keyed by the header. It ships with Python, so there's nothing to pip install, and it correctly handles commas inside quotes, which a plain split(",") does not. Below are working examples with their real output, plus how to convert numbers, skip the header, handle Excel files and write a CSV back out.
Pandas is brilliant for data analysis, but it's a big install for a script that just reads a spreadsheet export. On a school computer or a cheap web host you might not even be allowed to install it. Everything below was run with Python 3.13 and only the standard library.
I made a small file the way Excel saves one with "CSV UTF-8": Windows line endings, a hidden byte order mark (BOM) at the start, quoted fields containing commas, an accented name and one missing score. Real files tend to have at least one of these problems:
name,city,course,score
Aisha Khan,London,Web Development,78
Tom Riley,"Leeds, West Yorkshire",Python,64
Zoë Patel,Manchester,"Databases, SQL",91
Rahim Uddin,Dhaka,Web Development,
It's tempting to read the file line by line and split each line on commas. Here's what happens:
# The tempting way: split each line on commas
with open("students.csv", encoding="utf-8") as f:
for line in f:
print(line.strip().split(","))
# Output:
# ['\ufeffname', 'city', 'course', 'score']
# ['Aisha Khan', 'London', 'Web Development', '78']
# ['Tom Riley', '"Leeds', ' West Yorkshire"', 'Python', '64']
# ['Zoë Patel', 'Manchester', '"Databases', ' SQL"', '91']
# ['Rahim Uddin', 'Dhaka', 'Web Development', '']
Two things went wrong. "Leeds, West Yorkshire" was cut in half, so Tom's row now has five columns instead of four, with stray quote marks. And the first column name is '\ufeffname' because of the BOM. Your code would fail the moment it looked for a column called name. The csv module fixes both.
csv.reader gives you one list of strings per row. Calling next() on it once reads the header row, so the loop only sees the data:
import csv
with open("students.csv", newline="", encoding="utf-8-sig") as f:
reader = csv.reader(f)
header = next(reader) # first row: column names
print("header:", header)
for row in reader:
print(row)
# Output:
# header: ['name', 'city', 'course', 'score']
# ['Aisha Khan', 'London', 'Web Development', '78']
# ['Tom Riley', 'Leeds, West Yorkshire', 'Python', '64']
# ['Zoë Patel', 'Manchester', 'Databases, SQL', '91']
# ['Rahim Uddin', 'Dhaka', 'Web Development', '']
The quoted commas are handled correctly and the header is clean. Two arguments to open() do a lot of work here:
newline="" is what the official csv documentation asks for. It lets the csv module deal with line endings itself, which matters for quoted fields that contain line breaks.encoding="utf-8-sig" reads UTF-8 and quietly removes the BOM if there is one. If there isn't, it behaves exactly like "utf-8", so it's a safe default.import csv
with open("students.csv", newline="", encoding="utf-8") as f:
print(next(csv.reader(f))[0].encode()) # plain utf-8 keeps the BOM
with open("students.csv", newline="", encoding="utf-8-sig") as f:
print(next(csv.reader(f))[0].encode()) # utf-8-sig strips it
# Output:
# b'\xef\xbb\xbfname'
# b'name'
Those three bytes, \xef\xbb\xbf, are the BOM. With plain utf-8 they become part of the first column name; with utf-8-sig they're gone.
Using column positions like row[2] works, but it breaks if someone adds a column to the spreadsheet. csv.DictReader uses the header row as keys, so you can write row["course"] instead:
import csv
with open("students.csv", newline="", encoding="utf-8-sig") as f:
rows = list(csv.DictReader(f))
print(rows[1])
print(f"{rows[2]['name']} studies {rows[2]['course']} in {rows[2]['city']}")
# Output:
# {'name': 'Tom Riley', 'city': 'Leeds, West Yorkshire', 'course': 'Python', 'score': '64'}
# Zoë Patel studies Databases, SQL in Manchester
Wrapping the reader in list() loads every row into memory, which is fine for files of a few thousand lines. For very large files, loop over csv.DictReader(f) directly so you only hold one row at a time. If you're new to the f"..." syntax in the print line, I explain it in Python f-strings: formatting examples.
Every value the csv module gives you is a string, even "78". Before you can add or compare scores, convert them. Empty cells are the usual trap: int("") raises a ValueError, so I convert blanks to None instead:
import csv
def to_int(value):
"""Turn '78' into 78 and '' into None."""
value = value.strip()
return int(value) if value else None
with open("students.csv", newline="", encoding="utf-8-sig") as f:
students = [
{**row, "score": to_int(row["score"])}
for row in csv.DictReader(f)
]
marked = [s for s in students if s["score"] is not None]
average = sum(s["score"] for s in marked) / len(marked)
best = max(marked, key=lambda s: s["score"])
print(f"{len(students)} students, {len(marked)} with a score")
print(f"Average score: {average:.1f}")
print(f"Top student: {best['name']} ({best['score']})")
print("Missing a score:", [s["name"] for s in students if s["score"] is None])
# Output:
# 4 students, 3 with a score
# Average score: 77.7
# Top student: Zoë Patel (91)
# Missing a score: ['Rahim Uddin']
The list comprehension builds a new dictionary for each row with {**row, "score": ...}, copying the other columns and replacing the score with a number. If that syntax is new to you, my post on Python list comprehension examples covers it step by step. The :.1f in the average rounds it to one decimal place.
Some exports have no header row at all. With csv.reader you simply don't call next(). With DictReader, give it the column names yourself:
reader = csv.DictReader(f, fieldnames=["name", "city", "course", "score"])
When you pass fieldnames, the first line is treated as data rather than as the header. If your file does have a header and you pass fieldnames anyway, the header will show up as your first "row", so check which case you're in.
If Excel's region is set to a country that uses a comma as the decimal mark, it saves CSV files with semicolons between fields. Tell the reader with delimiter=";", and swap the decimal comma for a dot before calling float():
import csv
# Some European Excel exports use ; between fields and , as the decimal mark
with open("prices.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f, delimiter=";"):
price = float(row["price"].replace(",", "."))
print(row["name"], price)
# Output:
# Tea 1.2
# Coffee 2.5
Tab-separated files work the same way with delimiter="\t".
Writing is the mirror image of reading. Open the file with "w" and newline="", write the header once, then the rows:
import csv
passed = [
{"name": "Aisha Khan", "score": 78},
{"name": "Zoë Patel", "score": 91},
]
with open("passed.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "score"])
writer.writeheader()
writer.writerows(passed)
with open("passed.csv", encoding="utf-8") as f:
print(f.read())
# Output:
# name,score
# Aisha Khan,78
# Zoë Patel,91
The csv writer adds quotes automatically when a value contains a comma, a quote or a line break, so you never have to escape anything yourself. If the file is going to be opened in Excel and contains non-English characters like "ë", save it with encoding="utf-8-sig" so Excel recognises it as UTF-8.
KeyError: 'name': almost always the BOM. Use encoding="utf-8-sig", or print reader.fieldnames to see the real column names.UnicodeDecodeError: the file isn't UTF-8. Older Excel versions on Windows often save as cp1252, so try encoding="cp1252".newline="".ValueError: invalid literal for int(): an empty cell or a value like "78 " or "£1,200". Strip and clean the text before converting it.The csv module is perfect for reading, filtering and converting files, and for scripts that need to run anywhere. Pandas starts to pay off when you're grouping, joining several files, doing statistics on hundreds of thousands of rows, or plotting. For a portfolio project or a quick report from a spreadsheet, the standard library is usually all you need.
You can open it with open() and split lines yourself, but that breaks on quoted commas, as shown above. The csv module is part of the standard library, so import csv costs nothing and needs no installation.
Call next(reader) once before your loop. It reads the first row, which you can keep as the header or ignore. csv.DictReader does this for you and uses it as the dictionary keys.
Use rows = list(csv.DictReader(f)). Each item is a dictionary such as {"name": "Aisha Khan", "score": "78"}, with every value as a string.
The file was saved with a UTF-8 byte order mark, which Excel adds to "CSV UTF-8" files. Open it with encoding="utf-8-sig" and the mark is removed.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
Work out a First, 2:1 or 2:2 from module marks: credit-weighted averages, 30:70 or 1:2 year weightings, rounding at 69.5 and the mark you need.
How UK student loan repayments are worked out in 2026/27: per-period thresholds, 9% over, rounding down, two plans at once and a floating-point trap.