All right after playing around with it some more I think I've got it.. when I first asked the question I didn't know about operator overloading.
So, what's going on in this python session?
>>> from sympy import *
>>> x = Symbol(x)
>>> x + x
2*x
It turns out there's nothing special about how the interpreter evaluates the expression; the important thing is that python translates
x + x
into
x.__add__(x)
and Symbol inherits from the Basic class, which defines __add__(self, other) to return Add(self, other). (These classes are found in sympy.core.symbol, sympy.core.basic, and sympy.core.add if you want to take a look.)
So as Jerub was saying, Symbol.__add__() has a decorator called _sympifyit which basically converts the second argument of a function into a sympy expression before evaluating the function, in the process returning a function called __sympifyit_wrapper which is what I saw before.
Using objects to define operations is a pretty slick concept; by defining your own operators and string representations you can implement a trivial symbolic algebra system quite easily:
symbolic.py --
class Symbol(object):
    def __init__(self, name):
        self.name = name
    def __add__(self, other):
        return Add(self, other)
    def __repr__(self):
        return self.name
class Add(object):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def __repr__(self):
        return self.left + '+' + self.right
Now we can do:
>>> from symbolic import *
>>> x = Symbol('x')
>>> x+x
x+x
With a bit of refactoring it can easily be extended to handle all basic arithmetic:
class Basic(object):
    def __add__(self, other):
        return Add(self, other)
    def __radd__(self, other): # if other hasn't implemented __add__() for Symbols
        return Add(other, self)
    def __mul__(self, other):
        return Mul(self, other)
    def __rmul__(self, other):
        return Mul(other, self)
    # ...
class Symbol(Basic):
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return self.name
class Operator(Basic):
    def __init__(self, symbol, left, right):
        self.symbol = symbol
        self.left = left
        self.right = right
    def __repr__(self):
        return '{0}{1}{2}'.format(self.left, self.symbol, self.right)
class Add(Operator):
    def __init__(self, left, right):
        self.left = left
        self.right = right
        Operator.__init__(self, '+', left, right)
class Mul(Operator):
    def __init__(self, left, right):
        self.left = left
        self.right = right
        Operator.__init__(self, '*', left, right)
# ...
With just a bit more tweaking we can get the same behavior as the sympy session from the beginning.. we'll modify Add so it returns a Mul instance if its arguments are equal. This is a bit trickier since we have get to it before instance creation; we have to use __new__() instead of __init__():
class Add(Operator):
    def __new__(cls, left, right):
        if left == right:
            return Mul(2, left)
        return Operator.__new__(cls)
    ...
Don't forget to implement the equality operator for Symbols:
class Symbol(Basic):
    ...
    def __eq__(self, other):
        if type(self) == type(other):
            return repr(self) == repr(other)
        else:
            return False
    ...
And voila. Anyway, you can think of all kinds of other things to implement, like operator precedence, evaluation with substitution, advanced simplification, differentiation, etc., but I think it's pretty cool that the basics are so simple.