Why does `git checkout <branch> <file>` stage the change?

It's really an implementation detail that the Git authors chose to let show through.

Git cannot—or rather, at one point, could not—read files directly from the repository into the work-tree. It has (or had) to pass them through an intermediary first: it had to copy them, or at least their vital statistics,1 somewhere else. Only then could Git copy the data to a work-tree file.2 The "somewhere else" is an index entry. The index is also called the staging area.

When you git checkout an entire commit, this is what you want anyway. So the internal limitation, of copying to the index first and only then to the work-tree, was actually a plus. So this mechanism, of copying into the index first, and only then on into the work-tree, was embedded into the implementation. Then, eventually, the user-oriented git checkout front end gained the ability to check out one individual file, or some small subset of files ... and it continued to do so through the index. The implementation detail became a documented feature.

Note that sometimes, the index is in use as a helper area during a conflicted merge. In this case, for some file F, there are up to three entries, in numbered slots 1 (base), 2 (--ours), and 3 (--theirs), instead of just one entry in the normal slot-zero. When this is so, you can extract any of the three index slot entries to the work-tree without disturbing the index. But if you use git checkout to extract a file from some other commit-or-tree, Git copies the file into the index, writing it to slot zero. This has the side effect of removing the higher-numbered slots, resolving the merge conflict!

1The main one is the hash ID. As ElpieKay noted in a comment, Git has to resolve the commit hash to a tree hash, then search the various trees to find whichever file(s) is/are of interest, so that it can obtain the blob hash. The index entry itself has a bunch more data as well, though, including stat structure data for the work-tree file, to make Git go fast.

2You can still use this work-flow, by using git read-tree to copy a tree into the index, then using git checkout-index to copy the index to the work-tree. Originally, Git consisted of a bunch of shell scripts like git-checkout wrapped around some fundamental C-coded pieces like git-read-tree. (The names were all hyphenated like this and there was no front end git command.)

The Short Answer

  1. git checkout always copies items out of the index into the worktree.

  2. If you specify a commit other than the one you're on (e.g. the HEAD of another branch), checkout will always first copy the items from that commit into the index.

  3. Anything in the index that differs from HEAD will show as a "staged change". This is by definition.

See also Git checkout file from branch without changing index