runbeginner8 min readpart 6 of 6

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 everywhere

Why 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-learn

Virtual 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.txt

If 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)"
Defaulting to user installation because normal site-packages is not writeable

What 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) # works

Second 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 file

Both 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 after

Set it in your shell, or at the very top of the file above every import that might pull the hub in transitively.

More on running models locally