There's no reason why you must synchronize calls to send or recv as long as there is only a single sender or receiver. Consider if you had two threads attempting to receive a message. Let's say, for the sake of discussion, that a client=>server message is 8 bytes long with the first 4 bytes being a command and the second 4 bytes being an object identifier of some sort:
Thread A: sock.recv(8) obtains first 4 bytes
Thread B: sock.recv(8) obtains second 4 bytes
Here, neither thread ends up with a complete message. Now, many times that won't happen and you'd get the entire 8 byte message as a unit. But that is not guaranteed, and when the server / network get busy and OS network buffers fill up and you have multiple threads vying for CPU, it is more likely for this to happen. (And it's not just python BTW; the same would be true of a C program.)
So, yes, sockets are "thread-safe" in that exactly one thread's recv will obtain a given byte of data and there's nothing to prevent both threads from calling recv at the same time. The OS won't get confused by this. But there's nothing in a TCP (SOCK_STREAM) socket that will ensure that an entire "message" (of any length greater than a single byte) is received by one thread.
If you have two threads, with one only receiving and one only sending, then there is no competition and no locking would be required to maintain a coherent data stream.
UDP sockets OTOH are "message-oriented" so there isn't the same problem for them. Each send or recv delivers an indivisible datagram. A receiving thread won't get part of a datagram. It gets the whole datagram or nothing (though the datagram can be truncated due to insufficient buffer space; but in that case the remainder is simply dropped not delivered to the next receiving thread).