It is perfectly deterministic and simple. You need to be aware of the differences between bytes and unicode strings though. Read Pragmatic Unicode by Ned Batchelder.
a refers to a unicode string composed to three characters, so a evaluates to that and the Python REPL tries to print() it -- the first step is converting it to a str (which is just return self for strings). The resulting string is converted into bytes according to the encoding associated with stdout (e.g. your terminal), and sends these bytes there. Your terminal interprets the bytes it receives according to the encoding it uses.
a.encode('ascii') explicitly converts the unicode into a byte sequence, using the ASCII encoding. The result of that is a bytes object, and that result is print'd by the Python REPL. Again, the first step is converting it to a string -- because bytes are not strings, and implicit conversions between the two are very harmful as Python 2 encoding/decoding errors proves, you only get what repr also gives you: A string which is equivalent to a bytes literal in Python source code. This string is then printed as before, same difference.
In >>> print(a), you call print yourself with the same consequences as above, and the result of print is None so the Python REPL does not print it. Same for >>> print(a.encode('ascii')) and >>> print(str(a.encode('ascii'))), you just trigger the printing explicitly and do the str conversion beforehand instead of implicitly.
There is indeed an inverse of str.encode, it's indeed called decode and is a method of bytes objects. So some_str.encode(E).decode(E) can be a no-op. It can also lose information or alter the string along the way (e.g. by replacing unknown characters), or throw an exception if you do not ask it to lose information.
If you have bytes which represent some string in some encoding, you can use them for I/O. In fact, this is the "native" way of doing I/O -- all I/O and all networking is bytes. But print is for strings. Instead, in the case of file I/O (including stdout), you want to open them in binary mode and use the .write method. For sys.stdout specifically, text mode is used by default, but you can access a binary version as sys.stdout.buffer. Of course, if the bytes you send were created assuming a different encoding (or not intended as text at all), you'll get gibberish.