Testable python class which depends on another class?

Beware, instantiating an object as default value for a parameter will share the same object between deifferent calls (see this question).

As for your question, if you really to want the call to X the same, as in x = X() # self.c will be ready then you will need to patch the Config reference that your X.__init__ uses.
If you relax this constraint, meaning that you accept to do x = X(c=Config()) instead, it will be very easy to provide the "test double" of your choice (typically a "mock"), and ease testing. It is called dependency injection and facilitating testing is one of its main advantages.

EDIT: as you need to mock C.f, here is an example :

class C:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def f(self, z):  # <-- renamed to `z` to avoid confusion with the `x` instance of class `X`
        print(f"method F called with {z=}, {self.a=} and {self.b=}")
        return z + self.a + self.b


class X:
    def __init__(self, c: C = C(a=1, b=2)):
        self.c = c


# /!\ to do some mocking, I had to `pip install pytest_mock` (version 3.6.1) as `pytest` provides no mocking support
def test_my_class_X_with_C_mock(mocker):  # the `mocker` parameter is a fixture, it is automatically provided by the pytest runner
    x = X()  # create the X as always

    assert x.c.f(z=3) == 6  # prints "method F called with z=3, self.a=1 and self.b=2"

    def my_fake_function_f(z):  # create a function to call instead of the original `C.f` (optional)
        print(f"mock intercepted call, {z=}")
        return 99
    my_mock = mocker.Mock(  # define a mock
        **{"f.side_effect": my_fake_function_f})  # which has an `f`, which calls the fake function when called (side effect)
    mocker.patch.object(target=x, attribute="c", new=my_mock)  # patch : replace `x.c` with `my_mock` !

    assert x.c.f(z=3) == 99  # prints "mock intercepted call, z=3"


def main():
    # do the regular thing
    x = X()
    print(x.c.f(z=3))  # prints "method F called with z=3, self.a=1 and self.b=2" and the result "6"

    # then do the tests
    import pytest
    pytest.main(["-rP", __file__])  # run the test, similar to the shell command `pytest <filename.py>`, and with `-rP` show the print
    # it passes with no error !


if __name__ == "__main__":
    main()

Because C already exists (it is in the same file) in your example, it was not possible to simply mock.patch, it was required to mock.patch.object.
Also, it is possible to write simpler mocks, or just let patch create a plain Mock, but for the example I preferred to be explicit.