Migrating from pycocotools¶
hotcoco is a drop-in replacement for pycocotools. This guide covers the two migration paths and the few differences to be aware of.
Option 1: Change your imports¶
If you control the code that imports pycocotools, swap the import paths:
# Before
from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval
from pycocotools import mask as mask_util
# After
from hotcoco import COCO, COCOeval
from hotcoco import mask as mask_util
Everything else stays the same.
Option 2: Zero-code drop-in¶
If pycocotools is imported by a library you don't control, such as mmdet or detectron2, call init_as_pycocotools() once at startup:
from hotcoco import init_as_pycocotools
init_as_pycocotools()
# All pycocotools imports now resolve to hotcoco
from pycocotools.coco import COCO # → hotcoco.COCO
from pycocotools.cocoeval import COCOeval # → hotcoco.COCOeval
from pycocotools import mask # → hotcoco.mask
How the patch works, where to call it, and per-framework notes are in Framework integrations.
Method naming¶
Both camelCase and snake_case names are supported:
| pycocotools (camelCase) | hotcoco (snake_case) | Notes |
|---|---|---|
getAnnIds() |
get_ann_ids() |
Both work; only getAnnIds takes iscrowd=0/1 and areaRng=[] |
getCatIds() |
get_cat_ids() |
Both work |
getImgIds() |
get_img_ids() |
Both work |
loadAnns() |
load_anns() |
Both work |
loadCats() |
load_cats() |
Both work |
loadImgs() |
load_imgs() |
Both work |
loadRes() |
load_res() |
Both work |
annToRLE() |
ann_to_rle() |
Both work |
annToMask() |
ann_to_mask() |
Both work |
The same applies to Params properties: maxDets / max_dets, catIds / cat_ids, imgIds / img_ids, iouThrs / iou_thrs, recThrs / rec_thrs, areaRng / area_rng, areaRngLbl / area_rng_lbl, useCats / use_cats. OKS sigmas have one spelling, kpt_oks_sigmas — pycocotools also spells that attribute in snake_case.
And mask functions: toBbox / to_bbox, frPoly / fr_poly, frBbox / fr_bbox, frPyObjects / fr_py_objects.
Return types¶
hotcoco returns plain Python dicts and lists, matching pycocotools:
coco = COCO("instances_val2017.json")
anns = coco.load_anns([101])
print(type(anns[0])) # <class 'dict'>
Annotation dicts carry the same keys — id, image_id, category_id, bbox, area, segmentation, iscrowd — plus any custom fields your dataset defines.
Argument types¶
The pycocotools-named functions accept the argument types pycocotools accepts:
COCO()takes astror anyos.PathLike, such as apathlib.Path— see the constructor.- Id-list arguments of the query and load methods take an
intor any iterable of ints, such as asetordict.keys()— see Methods. getAnnIdstakesiscrowd=0/1andareaRng=[]— seeget_ann_ids.mask.mergetakesintersect=1— seemerge.mask.ioutakes boxes as well as RLEs, so aCOCOevalsubclass whosecomputeIoUcallsmaskUtils.iouon boxes works underinit_as_pycocotools()— seeiou.
get_ann_ids and hotcoco.primitives keep stricter, typed signatures.
Getters return copies — assign back to apply¶
The data lives in Rust, so coco.dataset returns a copy on each access, and
ev.coco_gt and ev.coco_dt return a new object that shares the evaluator's
data without copying it; an edit to that object, through dataset or
update_anns, lands on a private copy and never reaches the evaluator.
Attribute assignment
(ev.params.cat_ids = [...]) works — the setter routes the change back to Rust —
but mutating a container inside a copy is a no-op:
ev.params.cat_ids = [1, 2, 3] # applied — attribute assignment works
ev.params.maxDets.append(200) # no-op — mutates a temporary list
md = ev.params.maxDets # instead: pull, edit, assign back
md.append(200)
ev.params.maxDets = md
The same idiom applies to the dataset dict. pycocotools' in-memory construction
flow works because assigning coco.dataset re-indexes:
d = coco.dataset
d["annotations"].append(new_ann)
coco.dataset = d # replaces contents and rebuilds the index
coco.createIndex() # supported, but a formality after assignment
Editing annotations that are already there does not need the whole dataset:
coco.update_anns([{"id": ann_id, "area": mask_area}]) # the keys you pass, by id
See update_anns for what it raises and when it
re-indexes.
Known differences¶
| Behavior | pycocotools | hotcoco |
|---|---|---|
| Print on load | Prints "loading annotations..." to stdout | No progress output; loader warnings go to stderr and coco.load_warnings |
COCO() with no args |
Creates empty instance with print statements | Creates empty instance silently |
| Annotation IDs | Requires unique positive integers | Also accepts 0-based IDs |
getAnnIds(areaRng=...) on annotations missing area |
Raises KeyError |
Excludes them from the query |
Evaluating annotations missing area |
Raises KeyError |
Derives it in the evaluator's copy: the mask's pixel count, then the box's w × h, for ground truth; the box first for detections, as load_res does. The dataset keeps its missing areas |
Mutating coco.dataset / ev.params internals in place |
Mutates shared state | No-op on a copy — assign back to apply |
summarize() or run() with non-default params |
Prints the table only | Also emits a ReferenceDeviationWarning per deviation |
Summary stats when maxDets is not [1, 10, 100] |
Reads AP (stats[0]) at 100, or reports -1 when 100 is not a cap; reads the other AP lines and the last four AR lines at the third cap |
Reads every AP line and the last four AR lines at the largest cap, as faster-coco-eval does. With three caps only stats[0] differs: maxDets=[1, 10, 500] reports AP at 500 where pycocotools reports -1 |
A key or string holding a lone surrogate, such as "\ud800" |
Kept; json.dump writes it back escaped |
Raises UnicodeEncodeError or ValueError — text is stored as UTF-8, which has no form for a lone surrogate |
| Performance | Single-threaded C + Python | Multi-threaded Rust |
Metric parity¶
Every COCO metric matches pycocotools to floating-point precision — the measured differences on COCO val2017, and a script to check your own data, are in Metric parity.
Rust: module paths renamed in 1.0¶
For Rust users upgrading from 0.x, these module paths moved (crate-root re-exports
like hotcoco::COCOeval and hotcoco::Hierarchy are unchanged, so most code
needs no edits):
| Pre-1.0 module path | 1.0 |
|---|---|
hotcoco::eval |
hotcoco::detection |
hotcoco::hierarchy |
hotcoco::detection::hierarchy |
hotcoco::healthcheck |
hotcoco::quality::healthcheck |
hotcoco::types::{SummaryStats, CategoryStats, DatasetStats} |
hotcoco::quality |
hotcoco::primitives::counts |
hotcoco::metrics::counts |
There are no compatibility aliases for the old module paths. The Python API is unaffected.