The execute permissions are enough for the kernel to execute the file.
If the file starts with
#!, then it will see it's a script, parse that line to find out the path of the interpreter and an optional argument, and then execute that interpreter with that optional argument and the file path as argument.
For instance, if the file starts with:
#! /usr/bin/python -E
The kernel changes the
execve("/path/to/the-script", ["the-script", "arg"], [envs]) to
execve("/usr/bin/python", ["/usr/bin/python", "-E", "/path/to/the-script", "arg"], [envs]).
Without the execute permissions, it would never have gotten that far.
Now, at that point, what matters is the execution permission of the interpreter. It it's executables, then it works as normal.
/usr/bin/python will want to open the
/path/to/the-script to read and interpret the code in it. And for that, it will need read permission to the file. Unless maybe it has changed euid since last time (for instance if the
/usr/bin/python file had the suid/sgid bit), if you didn't have read permissions earlier, you still don't have it.
So you can execute a script alright if you have only execute permission to it. It's just that if the interpreter needs to open it to read its content that it fails (and you see the error message comes from the interpreter, not the shell you're trying to run that script from). In a script like:
#! /bin/echo Here goes
You'll see that not having read permission doesn't matter since
echo is not trying to open the file for reading.