Well, x is not exactly always empty: it will display the label itself if you hover over an exact value corresponding to an assigned label, excluding the extremities. So in your case, hovering on the exact central vertical axis of the figure will yield pi/2:

Not very helpful, I know.
As indicated by tacaswell's answer, you could re-implement ax.format_coord(). It does not actually require you to subclass axes, it can be much simpler than that
In your case, you can simply override ax.fmt_xdata():
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
x=np.linspace(0,np.pi)
y=np.sin(x)
ax.plot(x,y)
ax.set_xticks([0,np.pi/2,np.pi])
ax.set_xticklabels(['0','pi/2','pi'])
ax.fmt_xdata = lambda x: f"{x:.3f}" # <-- the magic happens here
plt.show()

You can even go really fancy and format x in terms of pi:
import fractions
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
x=np.linspace(0, np.pi)
y=np.sin(x)
ax.plot(x,y)
ax.set_xticks([0,np.pi/2,np.pi])
ax.set_xticklabels(['0','pi/2','pi'])
def fmt_xdata(val):
num, div = fractions.Fraction(val/np.pi).limit_denominator(100).as_integer_ratio()
num_str = f"{str(num)[:-1] if abs(num) == 1 else num}\u03C0" # 1pi => pi, -1pi => -pi
div_str = "" if div == 1 else f"/{div}" # pi/1 => pi
return f" {val:.3f} = {val/np.pi:.3f}\u03C0 \u2248 {num_str if num else '0'}{div_str} "
ax.fmt_xdata = fmt_xdata
plt.show()

['0',r'$\pi/2$',r'$\pi$']you can get the labels rendered as TeX.