I was able to solve my problem by putting a dummy 'marker' in as the MIMEApplication contents, then substituting in the real binary text after generating the MIME message:
from email.encoders import encode_noop
from email.generator import BytesGenerator
from email.mime.application import MIMEApplication
import io
# Actual binary "file" I want to encode (in real life, this is a file read from disk)
bytesToEncode = b'Q\x0dQ'
# unique marker that we can find and replace after message generation
binarymarker = b'GuadsfjfDaadtjhqadsfqerasdfiojBDSFGgg'
app = MIMEApplication(binarymarker, _encoder=encode_noop)
b = io.BytesIO()
g = BytesGenerator(b)
g.flatten(app, linesep='\r\n') # linesep for HTTP-compliant header line endings
# replace the marker with the actual binary data, then you have the output you want!
body = b.getvalue().replace(binarymarker, bytesToEncode)
After this, body
has the value I want, without messing up the binary bytes:
b'Content-Type: application/octet-stream\r\nMIME-Version: 1.0\r\n\r\nQ\rQ'
For a multi-part message, you just assemble the multipart message first, then do the replace() at the end.