How to control the source IP address of a ZeroMQ packet on a machine with multiple IPs?

Well, ZeroMQ is a bit tricky to read as a socket-"counterparty" ( it's not )

Why?

Classical socket is a free-to-harness resource.

ZeroMQ is a rather complex hierarchy of ideas and principles of behaviours ( better - distributed behaviours ), that help design smart distributed computing systems, without touching the low-level ( ZeroMQ well abstracted ) details, that control the actual flow of events in the storms of harsh conditions all distributed computing systems are open to face ( and have to handle at low level accordingly, if the high-level abstractions "promised" by ZeroMQ to keep are to be fulfilled and ease the designers' minds to focus rather on his/her core application part, not re-designing wheels ( with all trials and errors ) on pulling strings on O/S resources and shaking systems services for collecting just a few low-hanging types of fruits ).


For these reasons better straight forget ZeroMQ to be "something-like-socket"


ZeroMQ hierarchy in less than a five seconds

1:
ZeroMQ promises an easy re-use of a few trivial Scalable Formal Communication Pattern archetypes offering a particular distributed behaviour { PUB/SUB | PUSH/PULL | PAIR/PAIR | XPUB/XSUB | ... | REQ/REP }.

2:
Except a case of exclusively using just a device-less inproc:// transport-class, in all other cases, ZeroMQ needs one or more instances of a tunable "engine" - a Context( nIOthreads = N ), N >= 1.

3:
Having this, any ( future socket ) Access Point could get instantiated, bearing a behavioural archetype since the very moment of birth:

aSubscribeCHANNEL = aLocalCONTEXT.socket( zmq.SUB )      # this is NOT a <SOCKET>
#                                 ^^^^^^__________________ even it was typed in

4:
Having an "Access Point" instance ready "inside" the local "engine", one can lock-in its materialisation in the external-reality, using one or more ( yes, more ... WOW! Meaning more incoming pulling-strings into / whistles blowing out from a single Access Point "behaviour-node" ) calls to either of these methods:
.bind( <transport-class>://<a-class-specific-address> )
or
.connect( <transport-class>://<a-class-specific-address> )

5:
If and only if a .bind()-RTO-ready Access Point A "gets visited" by a first live .connect()-RTO-ready Access Point B, having any matching behaviour pairing, the ZeroMQ-messaging/signalling archetype gets live ( naming it also a socket was probably used for historical reasons, to ease an explanation in times )

( PUB/PUB will never fit, for obvious reasons, whereas PUB/SUB and many other behaviour-archetype pairs will and do lovely match and form the mutually-"compatible"-behaviours that will finally go live and stay so )


So,
how do I do the same thing with a Python ZeroMQ socket,
given a machine that has multiple addresses?

Simply use the fully qualified specification in a call to
.bind( "{ tcp | pgm | epgm }://<ip>:<port#>" ) method and you are done.

That easy.

Cool, isn't it?

Many further pleasant surprises under the hood of performance tuning, latency shaving and security tweaking.