Every error we hit building this, and the fix
The literal error messages from building a local image search tool, why each happened, and what fixed it.
Before you start
- none, this is a reference
These are real failures from building the tool in this series, in roughly the order they appeared. Every message here is copied verbatim, so searching for one should land you on this page.
ndarray.ptp does not exist in NumPy 2.0#
AttributeError: 'numpy.ndarray' object has no attribute 'ptp'What happened. NumPy 2.0 removed a batch of array methods that duplicated
module-level functions. ptp (peak to peak, meaning max minus min) was one of
them. The function form still exists.
Fix. Change the method call to the function call.
(sims - sims.min()) / (sims.ptp() + 1e-9) # breaks on NumPy 2.0
(sims - sims.min()) / (np.ptp(sims) + 1e-9) # works everywhereWhy it catches people. This is not specific to image search. Any code
written before mid-2024 that uses arr.ptp() breaks the moment someone
installs NumPy 2. The same removal hit arr.itemset() and arr.newbyteorder()
among others. If you inherit an older project and it dies on a method that
obviously should exist, check whether NumPy moved it to function-only.
The sklearn package is not called sklearn#
ModuleNotFoundError: No module named 'sklearn'Followed by, if you try the obvious fix:
The 'sklearn' PyPI package is deprecated, use 'scikit-learn'
rather than 'sklearn' for pip commands.What happened. You import sklearn but you install scikit-learn. The
sklearn name on PyPI is a deprecated placeholder that exists purely to fail
with that message.
Fix.
python -m pip install scikit-learnVirtual environment breaks after moving the folder#
Fatal error in launcher: Unable to create process using
'"C:\old\path\.venv\Scripts\python.exe" "C:\new\path\.venv\Scripts\pip.exe" install ...':
The system cannot find the file specified.What happened. Virtual environments store absolute paths inside their
launcher executables, their config, and their activate scripts. You moved or
copied the project folder and the venv came with it, still pointing at where
it used to live. Running python -m venv .venv over the top does not fix it,
because that command will not overwrite existing launcher executables.
Fix. Delete and recreate.
deactivate
rmdir /s /q .venv
python -m venv .venv
.venv\Scripts\activate
python -m pip install -r requirements.txtIf deactivate complains, close the terminal and open a new one before the
delete.
How to avoid the class of problem. Use python -m pip rather than bare
pip. It runs pip through the interpreter you are actually in and bypasses
the launcher executable entirely, so it would have worked even with a broken
venv. And check where you are before installing:
python -c "import sys; print(sys.executable)"Related: pip installs to the wrong place#
Defaulting to user installation because normal site-packages is not writeableWhat this is telling you. Your virtual environment is not active. Pip has
fallen back to a per-user install outside the project. Look at your prompt: if
there is no (.venv) prefix, activate it first.
Explorer opens Documents instead of your folder#
Symptom. You shell out to open a file manager and the wrong folder opens every time.
What happened. Windows argument parsing mangles the /select switch when
the path contains a space, and Explorer silently falls back to your Documents
folder rather than reporting an error.
subprocess.run(["explorer", f"/select,{p}"]) # breaks on spaces
subprocess.run(f'explorer /select,"{p}"', shell=True) # worksSecond trap in the same call. explorer returns a nonzero exit code even
when it succeeds, so check=True raises on a call that worked. Do not check
the return code for this specific command.
Tkinter file dialog hangs the server#
Symptom. A folder picker called from inside a web request handler either hangs forever or crashes the process, and which one depends on your platform.
What happened. Tkinter requires the main thread. Web framework request handlers do not run on it.
Fix. Run the dialog in its own process so it gets its own main thread.
import subprocess, sys
PICKER = ("import tkinter as tk\n"
"from tkinter import filedialog\n"
"r = tk.Tk(); r.withdraw(); r.attributes('-topmost', True)\n"
"print(filedialog.askdirectory() or '')\n")
r = subprocess.run([sys.executable, "-c", PICKER],
capture_output=True, text=True, timeout=300)
path = r.stdout.strip()Set -topmost because the dialog often opens behind the browser window. It
helps and does not fully solve it, so say so in your UI.
PIL warnings on real photo libraries#
UserWarning: Truncated File Read
UserWarning: Image appears to be a malformed MPO file, it will be
interpreted as a base JPEG fileBoth are harmless. The first appears when ImageFile.LOAD_TRUNCATED_IMAGES = True is set and PIL reads a file with a damaged tail. That setting is doing
its job, which is why the file is processed rather than skipped.
The second comes from phone photos that embed a second frame, typically for depth effects. PIL reading only the primary image is exactly what you want.
If a run reports zero skipped files alongside these warnings, nothing went wrong.
Indexing is far slower than expected#
Likely cause on Windows. Your photos are in OneDrive with files set to online-only. The paths resolve, so nothing errors, but reading each file triggers a download. Indexing works and crawls, and it quietly pulls your library back onto local disk.
Mark the folder as always available before a large index run, or accept that the first pass doubles as a download.
Thumbnails 404 and export fails after moving photos#
What happened. The index stores paths, not images. Move or rename the source folder and every path points at nothing.
Fix. Either re-index the new location, or prune the dead entries:
keep = [i for i, p in enumerate(paths) if Path(p).exists()]
np.save("vecs.npy", vecs[keep])
Path("paths.json").write_text(json.dumps([paths[i] for i in keep]))Better habit. Decide where your photo library lives before you index it.
Two clicks to reject looks like a double click#
Symptom. Marking a photo as "no" in a labeling UI also triggers whatever you bound to double click.
What happened. A three-state cycle on left click makes rejection a two-click action, and the browser reports two rapid clicks as a double click as well. Both handlers fire.
Fix. Do not put two interactions on the same element that can be confused with each other. Bind rejection to right click instead, which is one action and unambiguous.
This one is worth generalizing. If your most frequent action requires the same input pattern as an unrelated feature, the design is wrong regardless of what the event handlers say.
HF_HUB_OFFLINE appears to do nothing#
Symptom. You set it, but the process still reaches for the network.
What happened. You set it after importing the library. huggingface_hub
reads the variable at import time.
import os
os.environ["HF_HUB_OFFLINE"] = "1"
import open_clip # must come afterSet it in your shell, or at the very top of the file above every import that might pull the hub in transitively.