Bash did expand the star in your first example.
Assuming that the current directory contains only one file, main.py,
here is what happened:
$ seq 3 | xargs -I * echo *
# Expands to:
$ seq 3 | xargs -I main.py echo main.py
This asks to do echo main.py on each input, replacing main.py with
that input (because of -I main.py). And this is what you observed:
the three templates turn from echo main.py to echo 1, echo 2,
echo 3.
In the second case, the star being just next to -I prevented that
word from expanding (since no files match the pattern -I*):
$ seq 3 | xargs -I* echo *
# Expands to:
$ seq 3 | xargs -I* echo main.py
This asks xargs to replace instances of * (a literal star) in the
template echo main.py with the input. Since the template does not
include the placeholder * at all, it prints main.py each time.
(With failglob enabled in Bash, or in zsh with the default settings, the glob matching nothing would produce an error and the command would not run.)
If your command had protected both occurrences of *, then you would
have obtained the same output as in your first attempt:
$ seq 3 | xargs -I '*' echo '*'
1
2
3
If you happened to have more than one file in the directory, then the unquoted * might create problems more visibly. E.g. if there the two files main.py and other.py, then:
$ seq 3 | xargs -I * echo *
# Expands to:
$ seq 3 | xargs -I main.py other.py echo main.py other.py
and xargs will try to run other.py echo main.py other.py as the command (with main.py replaced by the current input string). That will likely give a "command not found" error from xargs. Or, if the second filename in sort order would match a runnable program in PATH, then xargs would run that program, and not echo.
A useful trick to get some amount of visibility into which Bash
expansions happen is to run set -o xtrace. With the flag set,
you can see the expansions on lines starting with +:
$ set -o xtrace
$ seq 3 | xargs -I * echo *
+ seq 3
+ xargs -I main.py echo main.py
1
2
3
$ seq 3 | xargs -I* echo *
+ seq 3
+ xargs '-I*' echo main.py
main.py
main.py
main.py
$ seq 3 | xargs -I '*' echo '*'
+ seq 3
+ xargs -I '*' echo '*'
1
2
3