Python Check if File Exists: pathlib, os.path and Safe Patterns
Path("x").is_file() checks for a file, is_dir() for a folder and exists() for either. Plus create-if-missing, delete-if-present, wildcards, empty files and race conditions.
To loop over a list in Python and get a counter that starts at 1, write for n, item in enumerate(items, start=1):. enumerate() returns pairs of (count, item), and the optional start argument sets the first number; it defaults to 0. You can also pass it positionally, as enumerate(items, 1). Changing start only changes the number you see, not which item comes first, so if you use that number to index the list, subtract one.
I ran every example in this post on Python 3.13.5 on 11 October 2026 and pasted the real output under each one. If you’re working through Python basics, this pairs well with my Python list comprehension examples, because enumerate() and comprehensions turn up together all the time.
The Python documentation for enumerate() gives the signature as enumerate(iterable, start=0). It returns an enumerate object, an iterator that yields tuples containing a count (starting from start) and the values from the iterable.
Here’s the most basic use:
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits):
print(i, fruit)
0 apple
1 banana
2 cherry
The i, fruit part is tuple unpacking. Each loop gets a tuple like (0, 'apple'), and Python splits it into two variables. You can name them anything; i and index are common.
Pass start=1. This is perfect for numbered lists that humans will read, where “item 0” would look odd:
for n, fruit in enumerate(fruits, start=1):
print(f"{n}. {fruit}")
1. apple
2. banana
3. cherry
The f-string here builds each line; my Python f-string formatting examples cover padding and alignment if you want the numbers to line up in columns.
Two things to keep straight:
start changes the counter, not the position. The first item is still fruits[0].fruits[n - 1], or simply keep the default start=0 for that loop.enumerate() doesn’t build a list. It returns a lazy iterator that produces one pair at a time, which is why it’s memory-friendly on large data. You can step through it by hand with next() or turn the rest into a list:
e = enumerate(fruits)
print(type(e).__name__, next(e), list(e))
print(list(enumerate("abc", 1)))
enumerate (0, 'apple') [(1, 'banana'), (2, 'cherry')]
[(1, 'a'), (2, 'b'), (3, 'c')]
Two details from that output. Once next(e) has taken the first pair, list(e) only gets what’s left: iterators are used up as you go. And enumerate() works on any iterable, including a string, where each “item” is a character.
There’s no magic inside enumerate(). The Python documentation shows that it’s equivalent to this small generator function:
def enumerate(iterable, start=0):
n = start
for elem in iterable:
yield n, elem
n += 1
Reading it line by line explains every behaviour in this post. The counter begins at whatever you pass as start. Each step hands back a tuple of the current number and the next element, then adds one. Because it uses yield, nothing is calculated until the loop asks for the next pair, which is why enumerate() works on huge files and endless generators without loading them into memory. And because it simply loops over iterable, it accepts anything a for loop accepts.
You’d never redefine the built-in like this in real code, since it would hide the faster C version, but writing it out once is a good way to understand generators.
Many beginners, me included, first write index loops like this:
for i in range(len(fruits)):
print(i, fruits[i])
0 apple
1 banana
2 cherry
It works, but enumerate() is usually better:
range(len(items)): No, you write items[i]; enumerate(items): Yes.range(len(items)): No, needs len() and indexing; enumerate(items): Yes.range(len(items)): range(1, len(items) + 1) and then items[i - 1]; enumerate(items): start=1.range(len(items)): More noise; enumerate(items): Says what you mean.range(len()) still makes sense when you genuinely only need the numbers, or when you’re comparing an item with its neighbour (items[i] and items[i + 1]).
You can’t change a list by assigning to the loop variable, because that only rebinds the name. You need the index, and enumerate() gives you both:
prices = [10.0, 12.5, 8.0]
for i, p in enumerate(prices):
prices[i] = round(p * 1.2, 2)
print(prices)
[12.0, 15.0, 9.6]
This adds 20% to each price. Notice I kept the default start=0 here because I’m using i as a real index.
Looping over a dict gives you its keys, so to get keys, values and a counter, call .items() and unpack the inner tuple with brackets:
scores = {"Ayesha": 91, "Ben": 73}
for rank, (name, score) in enumerate(scores.items(), start=1):
print(rank, name, score)
1 Ayesha 91
2 Ben 73
Dictionaries keep insertion order (guaranteed since Python 3.7), so the ranks follow the order you added the keys. To rank by score instead, sort the items first. With Chloe added on 88:
scores = {"Ayesha": 91, "Ben": 73, "Chloe": 88}
ranked = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
for rank, (name, score) in enumerate(ranked, start=1):
print(rank, name, score)
1 Ayesha 91
2 Chloe 88
3 Ben 73
zip() walks two lists side by side. Wrap it in enumerate() to number the pairs:
names = ["Ayesha", "Ben", "Chloe"]
marks = [91, 73, 88]
for i, (name, mark) in enumerate(zip(names, marks), start=1):
print(i, name, mark)
1 Ayesha 91
2 Ben 73
3 Chloe 88
The order matters: enumerate(zip(...)) gives (i, (name, mark)), so you need the brackets around (name, mark) when unpacking.
reversed() needs a sequence it can walk backwards, and an enumerate object isn’t one. Calling reversed(enumerate(fruits)) directly raised TypeError: 'enumerate' object is not reversible in my test. Convert it to a list first, which keeps the original numbers attached to each item:
for i, fruit in reversed(list(enumerate(fruits, start=1))):
print(i, fruit)
3 cherry
2 banana
1 apple
If you want the count to go 1, 2, 3 while the items go backwards, do it the other way round:
for i, f in enumerate(reversed(fruits), start=1):
print(i, f)
1 cherry
2 banana
3 apple
This version works without building a list, because reversed() runs on the original list, and enumerate() happily wraps any iterator.
Files are iterables of lines, so enumerate(f, start=1) gives you human-friendly line numbers for error messages. I’ve used an in-memory file here so the example runs anywhere, but a real open("scores.csv") works the same way:
import io
fake = io.StringIO("name,score\nAyesha,91\nBen,seventy\n")
for line_no, line in enumerate(fake, start=1):
if line_no == 1:
continue # skip the header
name, score = line.strip().split(",")
if not score.isdigit():
print(f"line {line_no}: bad score {score!r}")
line 3: bad score 'seventy'
Reporting “line 3” rather than “row 1 after the header” makes it easy to find the problem in a text editor. For proper CSV parsing with quoted fields, use the csv module, as in my guide to reading a CSV in Python without pandas.
enumerate() fits neatly into comprehensions. Two patterns I use a lot: building a lookup from item to position, and finding the positions of items that match a test:
print({fruit: i for i, fruit in enumerate(fruits)})
print([i for i, f in enumerate(fruits) if "an" in f])
{'apple': 0, 'banana': 1, 'cherry': 2}
[1]
The second one returns [1] because only “banana” contains “an”. It finds every match, unlike list.index(), which stops at the first.
Because you have the counter, you can act on every nth item with the modulo operator:
for i, f in enumerate(fruits):
if i % 2 == 0:
print(i, f)
0 apple
2 cherry
For simply taking every second item, slicing (fruits[::2]) is shorter. Use enumerate() when you also need the position, for example to print “row 3 of 10” or to add a separator before every item except the first (if i > 0:).
start is great for paginated lists. If page 2 shows items 11 to 20, you want the numbers to carry on from page 1 rather than restart at 1. Work out the offset once and pass offset + 1 as start:
results = [f"item-{n}" for n in range(1, 26)]
page, per_page = 2, 10
offset = (page - 1) * per_page
for n, item in enumerate(results[offset:offset + per_page], start=offset + 1):
print(n, item)
11 item-11
12 item-12
...
20 item-20
I’ve trimmed the middle lines of the output; the full run printed 11 to 20 in order. The same offset is what you’d pass to LIMIT ... OFFSET ... in SQL when fetching that page from a database.
For a grid (a list of lists), use one enumerate() per level to get row and column numbers. Here I look for every O on a small noughts-and-crosses board:
grid = [["X", "O", "X"], ["O", "X", "O"]]
for r, row in enumerate(grid):
for c, cell in enumerate(row):
if cell == "O":
print(f"O at row {r}, col {c}")
O at row 0, col 1
O at row 1, col 0
O at row 1, col 2
Use the default start=0 here, so the numbers match grid[r][c] exactly. That makes it easy to update a cell later with grid[r][c] = "X".
start can be any integer, including a negative one. It can’t be a float:
print(list(enumerate(fruits, start=-1)))
try:
list(enumerate(fruits, start=1.5))
except TypeError as err:
print("TypeError:", err)
[(-1, 'apple'), (0, 'banana'), (1, 'cherry')]
TypeError: 'float' object cannot be interpreted as an integer
Negative starts are rare, but they’re handy when your numbering needs an offset, such as counting relative to a reference row.
A common beginner bug is to call .index() inside a loop to find the current position. It returns the position of the first matching value, so it goes wrong as soon as the list has duplicates:
dupes = ["a", "b", "a"]
print([dupes.index(x) for x in dupes], [i for i, _ in enumerate(dupes)])
[0, 1, 0] [0, 1, 2]
The third item is at position 2, but .index() says 0. It’s also slow on big lists, because it searches from the beginning every time. enumerate() gives the correct position for free. The _ is a convention for “I don’t need this value”.
for i, item in enumerate(items), count first. for item, i in ... runs without error but gives you confusing names.for pair in enumerate(items) gives you tuples; use pair[0] and pair[1] or unpack straight away.items[n] with start=1 skips the first item and raises IndexError on the last.enumerate() again for a second loop.for loop shifts the positions under you, so items get skipped or processed twice. Loop over a copy (for i, x in enumerate(items[:]):) or build a new list instead.enumerate() only counts items. For sums, use a separate variable or itertools.accumulate().Pass the start argument: for n, item in enumerate(items, start=1):. You can also write enumerate(items, 1). The first item is still items[0]; only the counter changes.
It returns an enumerate object, a lazy iterator that yields (count, item) tuples. Wrap it in list() if you want to see all the pairs at once, for example list(enumerate(["a", "b"])) gives [(0, 'a'), (1, 'b')].
For everyday loops the speed difference is small, but enumerate() is cleaner, works with any iterable (including files and generators) and avoids indexing mistakes. Choose it for readability first.
Use .items() and unpack the inner pair: for i, (key, value) in enumerate(my_dict.items(), start=1):. Looping over the dict directly gives you only the keys.
Yes. Any integer works, so enumerate(items, start=-1) numbers the items -1, 0, 1 and so on. A float such as 1.5 raises a TypeError.
Use reversed(list(enumerate(items))) to keep each item’s original number while looping backwards, or enumerate(reversed(items), start=1) to count upwards over the reversed items.
Yes. enumerate() accepts any iterable. A string yields one character at a time, and an open file yields one line at a time, which makes enumerate(f, start=1) a neat way to get line numbers.
// 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.
Path("x").is_file() checks for a file, is_dir() for a folder and exists() for either. Plus create-if-missing, delete-if-present, wildcards, empty files and race conditions.
UAC (User Account Control) is the Windows feature that runs your apps with standard rights and asks before anything gets admin power. Here’s how the two tokens, the dimmed secure desktop, the four slider levels and the registry settings fit together.
Use NTFS for internal Windows drives, exFAT for USB sticks and SD cards you share with a Mac, and FAT32 only for small or old devices. Here are the real limits (4 GB files, 32 GB format cap), the security differences and the commands.
%windir% is an environment variable that points to the Windows folder, usually C:\Windows. Here’s what the common variables mean, the difference between user and system variables, set vs setx, PowerShell’s $Env: syntax and how PATH finds programs.