Repository: sympy/sympy

Issue:
Introducing Str class to core
This topic is briefly discussed in [google groups page](https://groups.google.com/u/2/g/sympy/c/tNm1LDcEXA4).

Currently, many classes use `Symbol` for instance naming. However, since the purpose of `Symbol` is to represent the mathematical scalar whose assumptions are defined, it would be better to introduce `Str` class for this purpose.

Any opinion, please?

Discussion:

Patch:
diff --git a/sympy/categories/baseclasses.py b/sympy/categories/baseclasses.py
index a29fa82e92..58b2dffc26 100644
--- a/sympy/categories/baseclasses.py
+++ b/sympy/categories/baseclasses.py
@@ -1,5 +1,6 @@
 from sympy.core import S, Basic, Dict, Symbol, Tuple, sympify
 from sympy.core.compatibility import iterable
+from sympy.core.symbol import Str
 from sympy.sets import Set, FiniteSet, EmptySet
 
 
@@ -196,8 +197,8 @@ def __new__(cls, domain, codomain, name):
         if not name:
             raise ValueError("Empty morphism names not allowed.")
 
-        if not isinstance(name, Symbol):
-            name = Symbol(name)
+        if not isinstance(name, Str):
+            name = Str(name)
 
         return Basic.__new__(cls, domain, codomain, name)
 
@@ -449,17 +450,17 @@ class Category(Basic):
     ========
     Diagram
     """
-    def __new__(cls, symbol, objects=EmptySet, commutative_diagrams=EmptySet):
-        if not symbol:
+    def __new__(cls, name, objects=EmptySet, commutative_diagrams=EmptySet):
+        if not name:
             raise ValueError("A Category cannot have an empty name.")
 
-        if not isinstance(symbol, Symbol):
-            symbol = Symbol(symbol)
+        if not isinstance(name, Str):
+            name = Str(name)
 
         if not isinstance(objects, Class):
             objects = Class(objects)
 
-        new_category = Basic.__new__(cls, symbol, objects,
+        new_category = Basic.__new__(cls, name, objects,
                                      FiniteSet(*commutative_diagrams))
         return new_category
 
diff --git a/sympy/core/symbol.py b/sympy/core/symbol.py
index d2eee90ea5..2918324430 100644
--- a/sympy/core/symbol.py
+++ b/sympy/core/symbol.py
@@ -1,6 +1,6 @@
 from sympy.core.assumptions import StdFactKB, _assume_defined
 from sympy.core.compatibility import is_sequence, ordered
-from .basic import Basic
+from .basic import Basic, Atom
 from .sympify import sympify
 from .singleton import S
 from .expr import Expr, AtomicExpr
@@ -15,6 +15,32 @@
 import re as _re
 import random
 
+class Str(Atom):
+    """
+    Represents string in SymPy.
+
+    Explanation
+    ===========
+
+    Previously, ``Symbol`` was used where string is needed in ``args`` of SymPy
+    objects, e.g. denoting the printing. However, since ``Symbol`` represents
+    mathematical scalar, this class should be used instead.
+
+    """
+    __slots__ = ('name',)
+
+    def __new__(cls, name, **kwargs):
+        if not isinstance(name, str):
+            raise TypeError("name should be a string, not %s" % repr(type(name)))
+        obj = Expr.__new__(cls, **kwargs)
+        obj.name = name
+        return obj
+
+    def __getnewargs__(self):
+        return (self.name,)
+
+    def _hashable_content(self):
+        return (self.name,)
 
 def _filter_assumptions(kwargs):
     """Split the given dict into assumptions and non-assumptions.
@@ -27,7 +53,6 @@ def _filter_assumptions(kwargs):
     Symbol._sanitize(assumptions)
     return assumptions, nonassumptions
 
-
 def _symbol(s, matching_symbol=None, **assumptions):
     """Return s if s is a Symbol, else if s is a string, return either
     the matching_symbol if the names are the same or else a new symbol
diff --git a/sympy/printing/printer.py b/sympy/printing/printer.py
index 8098ef5ad7..03bc9701b7 100644
--- a/sympy/printing/printer.py
+++ b/sympy/printing/printer.py
@@ -340,3 +340,6 @@ def _as_ordered_terms(self, expr, order=None):
             return list(expr.args)
         else:
             return expr.as_ordered_terms(order=order)
+
+    def _print_Str(self, s):
+        return self._print(s.name)
diff --git a/sympy/printing/repr.py b/sympy/printing/repr.py
index 04cfee5c3e..53270d9f0a 100644
--- a/sympy/printing/repr.py
+++ b/sympy/printing/repr.py
@@ -232,6 +232,9 @@ def _print_Sum2(self, expr):
         return "Sum2(%s, (%s, %s, %s))" % (self._print(expr.f), self._print(expr.i),
                                            self._print(expr.a), self._print(expr.b))
 
+    def _print_Str(self, s):
+        return "%s(%s)" % (s.__class__.__name__, self._print(s.name))
+
     def _print_Symbol(self, expr):
         d = expr._assumptions.generator
         # print the dummy_index like it was an assumption
diff --git a/sympy/vector/coordsysrect.py b/sympy/vector/coordsysrect.py
index 80230acd09..7040110642 100644
--- a/sympy/vector/coordsysrect.py
+++ b/sympy/vector/coordsysrect.py
@@ -3,10 +3,11 @@
 from sympy.core.compatibility import Callable
 from sympy.core.cache import cacheit
 from sympy.core import S, Dummy, Lambda
+from sympy.core.symbol import Str
 from sympy import symbols, MatrixBase, ImmutableDenseMatrix
 from sympy.solvers import solve
 from sympy.vector.scalar import BaseScalar
-from sympy import eye, trigsimp, ImmutableMatrix as Matrix, Symbol, sin, cos,\
+from sympy import eye, trigsimp, ImmutableMatrix as Matrix, sin, cos,\
     sqrt, diff, Tuple, acos, atan2, simplify
 import sympy.vector
 from sympy.vector.orienters import (Orienter, AxisOrienter, BodyOrienter,
@@ -87,8 +88,8 @@ def __new__(cls, name, transformation=None, parent=None, location=None,
                 transformation = Lambda((x1, x2, x3),
                                         transformation(x1, x2, x3))
             elif isinstance(transformation, str):
-                transformation = Symbol(transformation)
-            elif isinstance(transformation, (Symbol, Lambda)):
+                transformation = Str(transformation)
+            elif isinstance(transformation, (Str, Lambda)):
                 pass
             else:
                 raise TypeError("transformation: "
@@ -141,7 +142,7 @@ def __new__(cls, name, transformation=None, parent=None, location=None,
             lambda_lame = CoordSys3D._get_lame_coeff('cartesian')
             lambda_inverse = lambda x, y, z: r.inv()*Matrix(
                 [x-l[0], y-l[1], z-l[2]])
-        elif isinstance(transformation, Symbol):
+        elif isinstance(transformation, Str):
             trname = transformation.name
             lambda_transformation = CoordSys3D._get_transformation_lambdas(trname)
             if parent is not None:
@@ -165,7 +166,7 @@ def __new__(cls, name, transformation=None, parent=None, location=None,
         if variable_names is None:
             if isinstance(transformation, Lambda):
                 variable_names = ["x1", "x2", "x3"]
-            elif isinstance(transformation, Symbol):
+            elif isinstance(transformation, Str):
                 if transformation.name == 'spherical':
                     variable_names = ["r", "theta", "phi"]
                 elif transformation.name == 'cylindrical':
@@ -188,10 +189,10 @@ def __new__(cls, name, transformation=None, parent=None, location=None,
         # they may actually be 'coincident' wrt the root system.
         if parent is not None:
             obj = super().__new__(
-                cls, Symbol(name), transformation, parent)
+                cls, Str(name), transformation, parent)
         else:
             obj = super().__new__(
-                cls, Symbol(name), transformation)
+                cls, Str(name), transformation)
         obj._name = name
         # Initialize the base vectors
 
diff --git a/sympy/vector/orienters.py b/sympy/vector/orienters.py
index 241f1fa40d..bcaab04a8e 100644
--- a/sympy/vector/orienters.py
+++ b/sympy/vector/orienters.py
@@ -1,7 +1,8 @@
 from sympy.core.basic import Basic
 from sympy import (sympify, eye, sin, cos, rot_axis1, rot_axis2,
-                   rot_axis3, ImmutableMatrix as Matrix, Symbol)
+                   rot_axis3, ImmutableMatrix as Matrix)
 from sympy.core.cache import cacheit
+from sympy.core.symbol import Str
 import sympy.vector
 
 
@@ -104,7 +105,7 @@ class ThreeAngleOrienter(Orienter):
     """
 
     def __new__(cls, angle1, angle2, angle3, rot_order):
-        if isinstance(rot_order, Symbol):
+        if isinstance(rot_order, Str):
             rot_order = rot_order.name
 
         approved_orders = ('123', '231', '312', '132', '213',
@@ -137,7 +138,7 @@ def __new__(cls, angle1, angle2, angle3, rot_order):
         parent_orient = parent_orient.T
 
         obj = super().__new__(
-            cls, angle1, angle2, angle3, Symbol(rot_order))
+            cls, angle1, angle2, angle3, Str(rot_order))
         obj._angle1 = angle1
         obj._angle2 = angle2
         obj._angle3 = angle3
diff --git a/sympy/vector/point.py b/sympy/vector/point.py
index 4528c9e6db..e46bb22d91 100644
--- a/sympy/vector/point.py
+++ b/sympy/vector/point.py
@@ -1,8 +1,8 @@
 from sympy.core.basic import Basic
+from sympy.core.symbol import Str
 from sympy.vector.vector import Vector
 from sympy.vector.coordsysrect import CoordSys3D
 from sympy.vector.functions import _path
-from sympy import Symbol
 from sympy.core.cache import cacheit
 
 
@@ -25,9 +25,9 @@ def __new__(cls, name, position=Vector.zero, parent_point=None):
                     parent_point))
         # Super class construction
         if parent_point is None:
-            obj = super().__new__(cls, Symbol(name), position)
+            obj = super().__new__(cls, Str(name), position)
         else:
-            obj = super().__new__(cls, Symbol(name), position, parent_point)
+            obj = super().__new__(cls, Str(name), position, parent_point)
         # Decide the object parameters
         obj._name = name
         obj._pos = position


Test patch:
diff --git a/sympy/core/tests/test_args.py b/sympy/core/tests/test_args.py
index c9cac546ef..6bdc452c62 100644
--- a/sympy/core/tests/test_args.py
+++ b/sympy/core/tests/test_args.py
@@ -1749,6 +1749,10 @@ def test_sympy__stats__symbolic_multivariate_probability__CrossCovarianceMatrix(
     assert _test_args(CrossCovarianceMatrix(RandomMatrixSymbol('R', 3, 1),
                         RandomMatrixSymbol('X', 3, 1)))
 
+def test_sympy__core__symbol__Str():
+    from sympy.core.symbol import Str
+    assert _test_args(Str('t'))
+
 def test_sympy__core__symbol__Dummy():
     from sympy.core.symbol import Dummy
     assert _test_args(Dummy('t'))
diff --git a/sympy/core/tests/test_symbol.py b/sympy/core/tests/test_symbol.py
index a58ff66fbb..a07a1d2f36 100644
--- a/sympy/core/tests/test_symbol.py
+++ b/sympy/core/tests/test_symbol.py
@@ -1,10 +1,16 @@
 from sympy import (Symbol, Wild, GreaterThan, LessThan, StrictGreaterThan,
     StrictLessThan, pi, I, Rational, sympify, symbols, Dummy)
-from sympy.core.symbol import uniquely_named_symbol, _symbol
+from sympy.core.symbol import uniquely_named_symbol, _symbol, Str
 
 from sympy.testing.pytest import raises
 from sympy.core.symbol import disambiguate
 
+def test_Str():
+    a1 = Str('a')
+    a2 = Str('a')
+    b = Str('b')
+    assert a1 == a2 != b
+    raises(TypeError, lambda: Str())
 
 def test_Symbol():
     a = Symbol("a")
diff --git a/sympy/utilities/tests/test_pickling.py b/sympy/utilities/tests/test_pickling.py
index 35fae20dcc..c659e6ce8b 100644
--- a/sympy/utilities/tests/test_pickling.py
+++ b/sympy/utilities/tests/test_pickling.py
@@ -9,7 +9,7 @@
 from sympy.core.basic import Atom, Basic
 from sympy.core.core import BasicMeta
 from sympy.core.singleton import SingletonRegistry
-from sympy.core.symbol import Dummy, Symbol, Wild
+from sympy.core.symbol import Str, Dummy, Symbol, Wild
 from sympy.core.numbers import (E, I, pi, oo, zoo, nan, Integer,
         Rational, Float)
 from sympy.core.relational import (Equality, GreaterThan, LessThan, Relational,
@@ -92,6 +92,8 @@ def test_core_basic():
               SingletonRegistry, S):
         check(c)
 
+def test_core_Str():
+    check(Str('x'))
 
 def test_core_symbol():
     # make the Symbol a unique name that doesn't class with any other