I'd expect Python's socket.gethostname() to be just a thin wrapper around the gethostname system call, which on Ubuntu (using glibc) is in turn a wrapper for the uname system call:
C library/kernel differences
   The  GNU  C  library does not employ the gethostname() system call; instead, it implements
   gethostname() as a library function that calls uname(2) and copies up to  len  bytes  from
   the  returned  nodename  field  into  name.
And for uname(2):
   This is a system call, and the operating system presumably knows its  name,  release,  and
   version.   It  also  knows what hardware it runs on.  So, four of the fields of the struct
   are meaningful.  On the other hand, the field nodename is meaningless: it gives  the  name
   of  the present machine in some undefined network, but typically machines are in more than
   one network and have several names.  Moreover, the kernel has no way of knowing about such
   things,  so  it  has  to  be  told what to answer here.  The same holds for the additional
   domainname field.
   To this end, Linux uses the system calls sethostname(2) and setdomainname(2).   Note  that
   there  is no standard that says that the hostname set by sethostname(2) is the same string
   as the nodename field of the struct returned by uname()  (indeed,  some  systems  allow  a
   256-byte  hostname and an 8-byte nodename), but this is true on Linux.  The same holds for
   setdomainname(2) and the domainname field.
So:
It depends on what does sethostname(). In the case of Docker containers, that's Docker itself.
 
I don't know.
 
Just ask Docker to set whatever hostname you want for the container:
% docker run --rm -it python:3.9 python -c 'import socket; print(socket.gethostname())'
dd247ca179da
% docker run --rm -it --hostname my-own-hostname python:3.9 python -c 'import socket; print(socket.gethostname())'
my-own-hostname
 
Here's the relevant portion of the source code, for Python 3.11.3:
#ifdef HAVE_GETHOSTNAME
/* Python interface to gethostname(). */
/*ARGSUSED*/
static PyObject *
socket_gethostname(PyObject *self, PyObject *unused)
{
    if (PySys_Audit("socket.gethostname", NULL) < 0) {
        return NULL;
    }
#ifdef MS_WINDOWS
// snip
#else
    char buf[1024];
    int res;
    Py_BEGIN_ALLOW_THREADS
    res = gethostname(buf, (int) sizeof buf - 1);
    Py_END_ALLOW_THREADS
    if (res < 0)
        return set_error();
    buf[sizeof buf - 1] = '\0';
    return PyUnicode_DecodeFSDefault(buf);
#endif
}